[
  {
    "path": ".dockerignore",
    "content": "_screenshots\n.DS_Store\nREADME.md\nLICENSE\n.git\n.gitignore\n.github\n.env.local\n.env.test.local\nphpunit.xml.dist\n.php-cs*\n.phpunit.*\n.dockerignore\nvar/cache/*\nvar/log/*\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: ['https://www.paypal.me/tchap']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Something is not working as expected\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Before opening an issue, please:\n        - Check `./var/log/prod.log` for errors (only written when errors occur, thanks to the `fingers_crossed` filter)\n        - Check your web server / reverse proxy logs\n        - Read the [Troubleshooting section of the README](https://github.com/tchapi/davis/blob/main/README.md#troubleshooting)\n\n  - type: input\n    id: davis_version\n    attributes:\n      label: Davis version\n      description: Tag or commit SHA. Found on the `/` status page or via `git describe --tags`.\n      placeholder: \"e.g. v5.1.0\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: install_method\n    attributes:\n      label: Installation method\n      options:\n        - Docker – standalone (with Caddy)\n        - Docker – barebone (no reverse proxy)\n        - Bare metal / manual\n        - NixOS module\n        - Other (describe below)\n    validations:\n      required: true\n\n  - type: input\n    id: php_version\n    attributes:\n      label: PHP version (bare metal only)\n      description: Output of `php --version`. Skip if using Docker.\n      placeholder: \"e.g. 8.2.18\"\n\n  - type: dropdown\n    id: database\n    attributes:\n      label: Database\n      options:\n        - MySQL\n        - MariaDB\n        - PostgreSQL\n        - SQLite\n        - Other\n    validations:\n      required: true\n\n  - type: dropdown\n    id: reverse_proxy\n    attributes:\n      label: Reverse proxy\n      options:\n        - None (standalone Docker with Caddy)\n        - Nginx\n        - Apache\n        - Caddy\n        - Traefik\n        - Other\n    validations:\n      required: true\n\n  - type: dropdown\n    id: auth_method\n    attributes:\n      label: Authentication method\n      options:\n        - Internal (ADMIN_LOGIN / ADMIN_PASSWORD)\n        - IMAP\n        - LDAP\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: protocols\n    attributes:\n      label: Affected protocol(s)\n      options:\n        - label: CalDAV\n        - label: CardDAV\n        - label: WebDAV\n        - label: Admin dashboard\n        - label: API\n\n  - type: textarea\n    id: client\n    attributes:\n      label: Client(s) exhibiting the issue\n      description: Name, version, OS/platform. Add multiple if relevant.\n      placeholder: |\n        - DAVx⁵ 4.3.14 on Android 14\n        - Apple Calendar on macOS 14.4\n        - Thunderbird 115.10 on Ubuntu 22.04\n    validations:\n      required: true\n\n  - type: textarea\n    id: env_vars\n    attributes:\n      label: Relevant environment variables\n      description: |\n        Sanitize secrets (APP_SECRET, DATABASE_URL password, API_KEY, etc.).\n        Include at minimum: APP_ENV, CALDAV_ENABLED, CARDDAV_ENABLED, WEBDAV_ENABLED, AUTH_METHOD, and any env vars you think are relevant.\n      render: shell\n      placeholder: |\n        APP_ENV=prod\n        AUTH_METHOD=...\n        CALDAV_ENABLED=true\n        CARDDAV_ENABLED=true\n        WEBDAV_ENABLED=false\n        DATABASE_URL=mysql://user:***@host:3306/davis\n    validations:\n      required: true\n\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to reproduce\n      placeholder: |\n        1. Configure client with URL https://dav.example.com/dav\n        2. ...\n        3. Observe error\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected behaviour\n    validations:\n      required: true\n\n  - type: textarea\n    id: actual\n    attributes:\n      label: Actual behaviour\n      description: Include any error messages shown in the UI or client.\n    validations:\n      required: true\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs\n      description: |\n        Paste the relevant section of `./var/log/prod.log` (relative to your Davis installation root, **not** `/var/log`).\n        Also include web server / reverse proxy error logs if applicable.\n        Set `APP_ENV=dev` temporarily to get verbose output if `prod.log` is empty (you need to install dev dependencies with composer).\n      render: text\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional context\n      description: Anything else that might be relevant (docker-compose snippet, nginx config excerpt, network topology, etc.)."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Suggest an improvement or new functionality\nlabels: [\"feature request\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Please check [existing issues](https://github.com/tchapi/davis/issues) and the [Davis roadmap](https://github.com/users/tchapi/projects/1) first to avoid duplicates.\n\n  - type: textarea\n    id: problem\n    attributes:\n      label: Problem / motivation\n      description: What are you trying to do, and why is it currently not possible or inconvenient?\n      placeholder: \"e.g. I cannot do X, which forces me to...\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: solution\n    attributes:\n      label: Proposed solution\n      description: Describe the feature you have in mind. Be as specific as possible.\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives considered\n      description: Other approaches you have thought of or tried, and why they fall short.\n\n  - type: dropdown\n    id: area\n    attributes:\n      label: Area\n      options:\n        - CalDAV\n        - CardDAV\n        - WebDAV\n        - Admin dashboard\n        - API\n        - Authentication (IMAP / LDAP / internal)\n        - Docker / deployment\n        - Documentation\n        - Other\n    validations:\n      required: true\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional context\n      description: Links, screenshots, references to relevant RFCs or client behaviour, etc."
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main  # Only run on pushes to main\n  pull_request:\n    # Run on all PRs (no path restrictions)\n\nenv:\n  COMPOSER_ALLOW_SUPERUSER: '1'\n  SYMFONY_DEPRECATIONS_HELPER: max[self]=0\n  ADMIN_LOGIN: admin\n  ADMIN_PASSWORD: test\n\njobs:\n  dockerfile-checks:\n    name: Dockerfile Checks\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        dockerfile:\n          - docker/Dockerfile\n          - docker/Dockerfile-standalone\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Lint with Hadolint\n        uses: hadolint/hadolint-action@v3.1.0\n        with:\n          dockerfile: ${{ matrix.dockerfile }}\n          failure-threshold: warning\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Validate Dockerfile syntax\n        run: |\n          docker buildx build \\\n            --file ${{ matrix.dockerfile }} \\\n            --platform linux/amd64 \\\n            --check \\\n            .\n\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    container:\n      image: php:8.4-alpine\n      options: >-\n        --tmpfs /tmp:exec\n        --tmpfs /var/tmp:exec\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install GD / ZIP PHP extension\n        run: |\n          apk add $PHPIZE_DEPS libpng-dev libzip-dev\n          docker-php-ext-configure gd\n          docker-php-ext-configure zip\n          docker-php-ext-install gd zip\n\n      - name: Install Composer\n        run: wget -qO - https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet\n\n      - name: Validate Composer\n        run: composer validate\n\n      - name: Update to highest dependencies with Composer\n        run: composer install --no-interaction --no-progress --ansi\n\n      - name: Analyze\n        run: vendor/bin/php-cs-fixer fix --ansi\n\n  phpunit:\n    name: PHPUnit (PHP ${{ matrix.php }})\n    runs-on: ubuntu-latest\n    container:\n      image: php:${{ matrix.php }}-alpine\n      options: >-\n        --tmpfs /tmp:exec\n        --tmpfs /var/tmp:exec\n    services:\n      mysql:\n        image: mariadb:10.11\n        env:\n          # Corresponds to what is in .env.test\n          MYSQL_DATABASE: davis_test\n          MYSQL_USER: davis\n          MYSQL_PASSWORD: davis\n          MYSQL_ROOT_PASSWORD: root\n        options: >-\n          --health-cmd \"mysqladmin ping\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          - 3306:3306\n\n    strategy:\n      matrix:\n        php:\n          - '8.2'\n          - '8.3'\n          - '8.4'\n          - '8.5'\n      fail-fast: false\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install MySQL / GD / ZIP PHP extensions\n        run: |\n          apk add $PHPIZE_DEPS icu-libs icu-dev libpng-dev libzip-dev\n          docker-php-ext-configure intl\n          docker-php-ext-configure gd\n          docker-php-ext-configure zip\n          docker-php-ext-install pdo pdo_mysql intl gd zip\n\n      - name: Install Composer\n        run: wget -qO - https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet\n\n      - name: Install dependencies with Composer\n        run: composer install --no-progress --no-interaction --ansi\n\n      - name: Run tests with PHPUnit\n        env:\n          DATABASE_URL: mysql://davis:davis@mysql:3306/davis_test\n        run: vendor/bin/phpunit --process-isolation --colors=always\n\n  migrations:\n    name: Migrations (${{ matrix.database }})\n    runs-on: ubuntu-latest\n    container:\n      image: php:8.4-alpine\n      options: >-\n        --tmpfs /tmp:exec\n        --tmpfs /var/tmp:exec\n\n    services:\n      mysql:\n        image: mariadb:10.11\n        env:\n          MYSQL_DATABASE: davis_test\n          MYSQL_USER: davis\n          MYSQL_PASSWORD: davis\n          MYSQL_ROOT_PASSWORD: root\n        options: >-\n          --health-cmd \"mysqladmin ping\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n      postgres:\n        image: postgres:15-alpine\n        env:\n          POSTGRES_DB: davis_test\n          POSTGRES_USER: davis\n          POSTGRES_PASSWORD: davis\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n    strategy:\n      matrix:\n        database:\n          - mysql\n          - postgresql\n          - sqlite\n      fail-fast: false\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install database extensions\n        run: |\n          apk add $PHPIZE_DEPS icu-libs icu-dev libpng-dev libzip-dev postgresql-dev sqlite-dev\n          docker-php-ext-configure intl\n          docker-php-ext-configure gd\n          docker-php-ext-configure zip\n          docker-php-ext-install pdo pdo_mysql pdo_pgsql pdo_sqlite intl gd zip\n\n      - name: Install Composer\n        run: wget -qO - https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet\n\n      - name: Install dependencies with Composer\n        run: composer install --no-progress --no-interaction --ansi\n\n      - name: Run migrations (MySQL)\n        if: matrix.database == 'mysql'\n        env:\n          DATABASE_URL: mysql://davis:davis@mysql:3306/davis_test\n        run: |\n          php bin/console doctrine:database:create --if-not-exists --env=test\n          php bin/console doctrine:migrations:migrate --no-interaction --env=test\n          php bin/console doctrine:schema:validate --env=test\n\n      - name: Run migrations (PostgreSQL)\n        if: matrix.database == 'postgresql'\n        env:\n          DATABASE_URL: postgresql://davis:davis@postgres:5432/davis_test?serverVersion=15&charset=utf8\n        run: |\n          php bin/console doctrine:database:create --if-not-exists --env=test\n          php bin/console doctrine:migrations:migrate --no-interaction --env=test\n          php bin/console doctrine:schema:validate --skip-sync --env=test\n\n      - name: Run migrations (SQLite)\n        if: matrix.database == 'sqlite'\n        env:\n          DATABASE_URL: sqlite:///%kernel.project_dir%/var/data_test.db\n        run: |\n          php bin/console doctrine:migrations:migrate --no-interaction --env=test\n          php bin/console doctrine:schema:validate --skip-sync --env=test\n\n  smoke-test:\n    name: Application Smoke Test\n    runs-on: ubuntu-latest\n    container:\n      image: php:8.4-alpine\n      options: >-\n        --tmpfs /tmp:exec\n        --tmpfs /var/tmp:exec\n\n    services:\n      mysql:\n        image: mariadb:10.11\n        env:\n          MYSQL_DATABASE: davis_test\n          MYSQL_USER: davis\n          MYSQL_PASSWORD: davis\n          MYSQL_ROOT_PASSWORD: root\n        options: >-\n          --health-cmd \"mysqladmin ping\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install extensions and curl\n        run: |\n          apk add $PHPIZE_DEPS icu-libs icu-dev libpng-dev libzip-dev curl\n          docker-php-ext-configure intl\n          docker-php-ext-configure gd\n          docker-php-ext-configure zip\n          docker-php-ext-install pdo pdo_mysql intl gd zip\n\n      - name: Install Composer\n        run: wget -qO - https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet\n\n      - name: Install dependencies with Composer\n        run: composer install --no-progress --no-interaction --ansi --optimize-autoloader\n\n      - name: Prepare application\n        env:\n          DATABASE_URL: mysql://davis:davis@mysql:3306/davis_test\n          APP_ENV: test\n        run: |\n          php bin/console doctrine:database:create --if-not-exists --env=test\n          php bin/console doctrine:migrations:migrate --no-interaction --env=test\n          php bin/console cache:clear --env=test\n\n      - name: Start Symfony server in background\n        env:\n          DATABASE_URL: mysql://davis:davis@mysql:3306/davis_test\n          APP_ENV: test\n        run: |\n          php -S 127.0.0.1:8000 -t public/ &\n          echo $! > server.pid\n          sleep 3\n\n      - name: Test application responds\n        run: |\n          # Test that the app responds with a successful HTTP status\n          RESPONSE=$(curl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:8000/)\n          echo \"HTTP Response code: $RESPONSE\"\n          if [ \"$RESPONSE\" -ge 200 ] && [ \"$RESPONSE\" -lt 400 ]; then\n            echo \"✅ Application is responding correctly\"\n          else\n            echo \"❌ Application returned unexpected status code: $RESPONSE\"\n            exit 1\n          fi\n\n      - name: Test dashboard\n        continue-on-error: true\n        run: |\n          curl -f http://127.0.0.1:8000/dashboard || echo \"No health endpoint available\"\n\n      - name: Stop server\n        if: always()\n        run: |\n          if [ -f server.pid ]; then\n            kill $(cat server.pid) || true\n          fi\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Publish Docker image\n\non:\n  workflow_dispatch:\n  release:\n    types: [published]\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  # Use docker.io for Docker Hub if empty\n  REGISTRY: ghcr.io\n  # github.repository as <account>/<repo>\n  ACCOUNT: tchapi\n\njobs:\n  build:\n    name: Build Docker images\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        image:\n          - davis\n          - davis-standalone\n        platform:\n          - linux/amd64\n          - linux/arm64\n        include:\n          - image: davis\n            dockerfile: docker/Dockerfile\n          - image: davis-standalone\n            dockerfile: docker/Dockerfile-standalone\n    \n    steps:\n      - name: Prepare\n        run: |\n          platform=${{ matrix.platform }}\n          echo \"PLATFORM_PAIR=${platform//\\//-}\" >> $GITHUB_ENV\n      \n      - name: Checkout\n        uses: actions/checkout@v4\n      \n      - name: Extract metadata\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          context: git\n          images: ${{ env.REGISTRY }}/${{ env.ACCOUNT }}/${{ matrix.image }}\n          tags: type=raw,value=\n      \n      - name: Set up QEMU\n        if: matrix.platform == 'linux/arm64'\n        uses: docker/setup-qemu-action@v3\n      \n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          version: latest\n      \n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      \n      - name: Build and Push by digest\n        id: build\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ${{ matrix.dockerfile }}\n          platforms: ${{ matrix.platform }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          annotations: ${{ steps.meta.outputs.annotations }}\n          outputs: type=image,name=${{ env.REGISTRY }}/${{ env.ACCOUNT }}/${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true\n          cache-from: type=gha,scope=${{ matrix.image }}-${{ env.PLATFORM_PAIR }}\n          cache-to: type=gha,mode=max,scope=${{ matrix.image }}-${{ env.PLATFORM_PAIR }}\n      \n      - name: Export digest\n        run: |\n          mkdir -p /tmp/digests/\n          digest=\"${{ steps.build.outputs.digest }}\"\n          touch \"/tmp/digests/${digest#sha256:}\"\n      \n      - name: Upload digest\n        uses: actions/upload-artifact@v4\n        with:\n          name: digests-${{ matrix.image }}_${{ env.PLATFORM_PAIR }}\n          path: /tmp/digests/*\n          if-no-files-found: error\n          retention-days: 1\n      \n      - name: Build summary\n        run: |\n          echo \"### ✅ Build Complete\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Image**: \\`${{ matrix.image }}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Platform**: \\`${{ matrix.platform }}\\`\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Digest**: \\`${{ steps.build.outputs.digest }}\\`\" >> $GITHUB_STEP_SUMMARY\n\n  merge:\n    name: Create merged manifest\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        image:\n          - davis\n          - davis-standalone\n    needs: build\n    \n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      \n      - name: Download digests\n        uses: actions/download-artifact@v4\n        with:\n          path: /tmp/digests/${{ matrix.image }}/\n          pattern: digests-${{ matrix.image }}_*\n          merge-multiple: true\n      \n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          version: latest\n      \n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          context: git\n          images: ${{ env.REGISTRY }}/${{ env.ACCOUNT }}/${{ matrix.image }}\n          tags: |\n            type=semver,pattern={{version}}\n            type=edge,branch=${{ github.ref_name }}\n      \n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      \n      - name: Create manifest list and push\n        working-directory: /tmp/digests/${{ matrix.image }}/\n        run: |\n          docker buildx imagetools create \\\n            $(jq -cr '.tags | map(\"-t \" + .) | join(\" \")' <<< \"$DOCKER_METADATA_OUTPUT_JSON\") \\\n            --annotation index:org.opencontainers.image.created=\"${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}\" \\\n            --annotation index:org.opencontainers.image.description=\"${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.description'] }}\" \\\n            --annotation index:org.opencontainers.image.version=\"${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}\" \\\n            --annotation index:org.opencontainers.image.licenses=\"${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.licenses'] }}\" \\\n            --annotation index:org.opencontainers.image.title=\"${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.title'] }}\" \\\n            --annotation index:org.opencontainers.image.source=\"${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.source'] }}\" \\\n            --annotation index:org.opencontainers.image.url=\"${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.url'] }}\" \\\n            --annotation index:org.opencontainers.image.revision=\"${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}\" \\\n            $(printf '${{ env.REGISTRY }}/${{ env.ACCOUNT }}/${{ matrix.image }}@sha256:%s ' *)\n      \n      - name: Inspect image\n        run: |\n          docker buildx imagetools inspect \\\n            ${{ env.REGISTRY }}/${{ env.ACCOUNT }}/${{ matrix.image }}:${{ steps.meta.outputs.version }}"
  },
  {
    "path": ".gitignore",
    "content": "###> symfony/framework-bundle ###\n/.env.local\n/.env.local.php\n/.env.*.local\n/config/secrets/prod/prod.decrypt.private.php\n/public/bundles/\n/var/\n/vendor/\n###< symfony/framework-bundle ###\n\n###> symfony/phpunit-bridge ###\n.phpunit\n.phpunit.result.cache\n/phpunit.xml\n###< symfony/phpunit-bridge ###\n###> friendsofphp/php-cs-fixer ###\n/.php_cs.cache\n###< friendsofphp/php-cs-fixer ###\n\n.DS_Store\nTODO.todo\nwebdav_*"
  },
  {
    "path": ".hadolint.yaml",
    "content": "ignored:\n  - DL3018  # We don't pin apk versions (Alpine is rolling)\nfailure-threshold: error  # Only fail on errors, not warnings"
  },
  {
    "path": ".php-cs-fixer.php",
    "content": "<?php\n\n$finder = (new PhpCsFixer\\Finder())\n    ->in(__DIR__)\n    ->exclude('var')\n;\n\nreturn (new PhpCsFixer\\Config())\n    ->setRules([\n        '@Symfony' => true,\n        'ordered_imports' => true,                      // Order \"use\" alphabetically\n        'array_syntax' => ['syntax' => 'short'],        // Replace array() by []\n        'no_useless_return' => true,                    // Keep return null;\n        'phpdoc_order' => true,                         // Clean up the /** php doc */\n        'linebreak_after_opening_tag' => true,\n        'multiline_whitespace_before_semicolons' => false,\n        'phpdoc_add_missing_param_annotation' => true,\n        'single_trait_insert_per_statement' => false\n    ])\n    ->setUnsupportedPhpVersionAllowed(true)\n    ->setUsingCache(false)\n    ->setFinder($finder)\n;\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 tchap\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "Davis\n---\n\n[![Build Status][ci_badge]][ci_link]\n[![Publish Docker image](https://github.com/tchapi/davis/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/tchapi/davis/actions/workflows/main.yml)\n[![Latest release][release_badge]][release_link]\n[![License](https://img.shields.io/github/license/tchapi/davis)](https://github.com/tchapi/davis/blob/main/LICENSE)\n![Platform](https://img.shields.io/badge/platform-amd64%20%7C%20arm64-blue?logo=docker)\n![PHP Version](https://img.shields.io/badge/php-8.2%20%7C%208.3%20%7C%208.4-777BB4?logo=php&logoColor=white)\n[![Sponsor me][sponsor_badge]][sponsor_link]\n\nA modern, simple, feature-packed, fully translatable DAV server, admin interface and frontend based on `sabre/dav`, built with [Symfony 7](https://symfony.com/) and [Bootstrap 5](https://getbootstrap.com/), initially inspired by [Baïkal](https://github.com/sabre-io/Baikal) (_see dependencies table below for more detail_)\n\n### Web admin dashboard\n\nProvides user edition, calendar creation and sharing, and address book creation. The interface is simple and straightforward, responsive, and provides a light and a dark mode.\n\nSupports **Basic authentication**, as well as **IMAP** and **LDAP** (_via external providers_).\n\n### DAV Server\n\nThe underlying server implementation supports (*non-exhaustive list*) CalDAV, CardDAV, WebDAV, calendar sharing, scheduling, mail notifications, and server-side subscriptions (*depending on the capabilities of the client*).\n\n### Additional features ✨\n\n- Subscriptions (to be added via the client, such as the macOS calendar, for instance)\n- Public calendars, available to anyone with the link\n- Automatic birthday calendar, updated on the fly when birthdates change in your contacts\n\n### Deployment\n\nEasily containerisable (_`Dockerfile` and sample `docker-compose` configuration file provided_).\n\nNixOS [package](https://search.nixos.org/packages?channel=unstable&show=davis&from=0&size=50&sort=relevance&type=packages&query=davis) and module available.\n\nComes with already built Docker images in two flavours: [standalone](https://github.com/tchapi/davis/pkgs/container/davis-standalone) (with included Caddy reverse proxy) or [barebone](https://github.com/tchapi/davis/pkgs/container/davis).\n\n- - -\n\n✨ Created and maintained (with the help of the community) by [@tchapi](https://github.com/tchapi). ✨\n\n![Dashboard page](_screenshots/dashboard.png)\n![User creation page](_screenshots/user.png)\n![Sharing page](_screenshots/sharing.png)\n\n| Dark / Light mode  | Useful information at hand        |\n|--------------------|----------------------------|\n| ![Color mode](_screenshots/mode.png)| ![Setup information](_screenshots/setup_info.png)|\n\n# 🔩 Requirements\n\n  - PHP > 8.2 (with `pdo_mysql` [or `pdo_pgsql`, `pdo_sqlite`], `gd` and `intl` extensions), compatible up to PHP 8.5 (_See dependencies table below_)\n  - A compatible database layer, such as MySQL or MariaDB (recommended), PostgreSQL (not extensively tested yet) or SQLite (not extensively tested yet)\n  - Composer > 2 (_The last release compatible with Composer 1 is [v1.6.2](https://github.com/tchapi/davis/releases/tag/v1.6.2)_)\n  - The [`imap`](https://www.php.net/manual/en/imap.installation.php) and [`ldap`](https://www.php.net/manual/en/ldap.installation.php) PHP extensions if you want to use either authentication methods (_these are not enabled / compiled by default except in the Docker image_)\n\nDependencies\n------------\n\n| Release            | Status                     | PHP version        |\n|--------------------|----------------------------|--------------------|\n| `main` (edge)      | development branch         | PHP 8.2+           |\n| `v5.x`             | stable                     | PHP 8.2+           |\n| `v4.x`             | security fixes only        | PHP 8.0 → 8.3      |\n| `v3.x`             | :warning: unmaintained     | PHP 7.3 → 8.2      |\n\n# 🧰 Installation\n\n0. Clone this repository\n\n1. Retrieve the dependencies:\n\n    a. If you plan to run Davis locally, for development purposes\n\n    ```\n    composer install\n    ```\n\n    b. If you plan to run Davis on production\n\n    ```\n    composer install --no-dev\n    ```\n\n   And set `APP_ENV=prod` in your `.env.local` file (see below)\n\n\n3. At least put the correct credentials to your database (driver and url) in your `.env.local` file so you can easily create the necessary tables.\n\n4. Run the migrations to create all the necessary tables:\n\n    ```\n    bin/console doctrine:migrations:migrate\n    ```\n\n**Davis** can also be used with a pre-existing MySQL database (_for instance, one previously managed by Baïkal_). See the paragraph \"Migrating from Baikal\" for more info.\n\n> [!NOTE]\n>\n> The tables are not _exactly_ equivalent to those of Baïkal, and allow for a bit more room in columns for instance (among other things)\n\n## Configuration\n\nCreate your own `.env.local` file to change the necessary variables, if you plan on using `symfony/dotenv`.\n\n> [!NOTE]\n>\n> If your installation is behind a web server like Apache or Nginx, you can setup the env vars directly in your Apache or Nginx configuration (see below). Skip this part in this case.\n\n> [!CAUTION]\n>\n> In a production environnement, the `APP_ENV` variable MUST be set to `prod` to prevent leaking sensitive data.\n\n**a. The database driver and url** (_you should already have it configured since you created the database previously_)\n\n```shell\nDATABASE_DRIVER=mysql # or postgresql, or sqlite\nDATABASE_URL=mysql://db_user:db_pass@host:3306/db_name?serverVersion=10.9.3-MariaDB&charset=utf8mb4\n```\n\n**b. The admin password for the backend**\n\n```shell\nADMIN_LOGIN=admin\nADMIN_PASSWORD=test\n```\n\n> [!NOTE]\n>\n> You can bypass auth entirely if you use a third party authorization provider such as Authelia. In that case, set the `ADMIN_AUTH_BYPASS` env var to `true` (case-sensitive, this is actually the string `true`, not a boolean) to allow full access to the dashboard. This does not change the behaviour of the DAV server.\n\n**c. The auth Realm and method for HTTP auth**\n\n```shell\nAUTH_REALM=SabreDAV\nAUTH_METHOD=Basic # can be \"Basic\", \"IMAP\" or \"LDAP\"\n```\n> See [the following paragraph](#specific-environment-variables-for-imap-and-ldap-authentication-methods) for more information if you choose either IMAP or LDAP.\n\n**d. The global flags to enable CalDAV, CardDAV and WebDAV**. You can also disable the option to have calendars public\n\n```shell\nCALDAV_ENABLED=true\nCARDDAV_ENABLED=true\nWEBDAV_ENABLED=false\n\nPUBLIC_CALENDARS_ENABLED=true\n```\n\n> [!NOTE]\n>\n> By default, `PUBLIC_CALENDARS_ENABLED` is true. That doesn't mean that all calendars are public by default — it just means that you have an option, upon calendar creation, to set the calendar public (but it's not public by default).\n\n\n**e. Mailer configuration**\n\nIt includes:\n  - the mailer uri (`MAILER_DSN`)\n  - The email address that your invites are going to be sent from\n\n```shell\nMAILER_DSN=smtp://user:pass@smtp.example.com:port\nINVITE_FROM_ADDRESS=no-reply@example.org\n```\n\n> [!WARNING]\n> If the username, password or host contain any character considered special in a URI (such as `: / ? # [ ] @ ! $ & ' ( ) * + , ; =`), you MUST encode them.\n> See [here](https://symfony.com/doc/current/mailer.html#transport-setup) for more details.\n\n**f. The reminder offset for all birthdays**\n\nYou must specify a relative duration, as specified in [the RFC 5545 spec](https://www.rfc-editor.org/rfc/rfc5545.html#section-3.3.6)\n\n```shell\nBIRTHDAY_REMINDER_OFFSET=PT9H\n```\n\nIf you don't want a reminder for birthday events, set it to the `false` value (lowercase):\n\n```shell\nBIRTHDAY_REMINDER_OFFSET=false\n```\n\n> [!NOTE]\n>\n> By default, if the env var is not set or empty, we use `PT9H` (9am on the date of the birthday).\n\n**g. The paths for the WebDAV installation**\n\n> [!TIP]\n>\n> I recommend that you use absolute directories so you know exactly where your files reside.\n\n```shell\nWEBDAV_TMP_DIR=/webdav/tmp\nWEBDAV_PUBLIC_DIR=/webdav/public\nWEBDAV_HOMES_DIR=\n```\n\n> [!NOTE]\n>\n> In a docker setup, I recommend setting `WEBDAV_TMP_DIR` to `/tmp`.\n\n> [!NOTE]\n>\n> By default, home directories are disabled totally (the env var is set to an empty string). If needed, it is recommended to use a folder that is **NOT** a child of the public dir, such as `/webdav/homes` for instance, so that users cannot access other users' homes.\n\n**h. The log file path**\n\nYou can use an absolute file path here, and you can use Symfony's `%kernel.logs_dir%` and `%kernel.environment%` placeholders if needed (as in the default value). Setting it to `/dev/null` will disable logging altogether.\n\n```shell\nLOG_FILE_PATH=\"%kernel.logs_dir%/%kernel.environment%.log\"\n```\n\n**i. The timezone you want for the app**\n\nThis must comply with the [official list](https://www.php.net/manual/en/timezones.php)\n\n```shell\nAPP_TIMEZONE=Australia/Lord_Howe\n```\n\n> Set a void value like so:\n> ```shell\n> APP_TIMEZONE=\n> ```\n> in your environment file if you wish to use the **actual default timezone of the server**, and not enforcing it.\n\n**j. Trusting forwarded headers**\n\nIf you're behind one or several proxies, the TLS termination might be upstream and the application might not be aware of the HTTPS context. In order for urls to be generated with the correct scheme, you should indicate that you trust the chain of proxies until the TLS termination one. You can use the Symfony mechanism for that (see [documentation](https://symfony.com/doc/7.2/deployment/proxies.html) for possible values):\n\n```shell\nSYMFONY_TRUSTED_PROXIES=127.0.0.1,REMOTE_ADDR\n```\n\n#### Overriding the dotenv (`.env`) path\n\nYou can override the expected location of the environment files (`.env`, `.env.local`, etc) by setting the `ENV_DIR` variable.\n\nThe value should be to a _folder_ containing the env files. This value must be specified in the actual environment and *not* in an `.env` file as it is read and evaluated **before** the env files are read.\n\nFor instance, you can use it to call `bin/console` with a specific dotenv directory:\n\n```shell\n> ENV_DIR=/var/lib/davis bin/console\n```\n\nOr use it directly in the Apache configuration\n\n```apache\n<VirtualHost *:80>\n    # .. rest of config (see ¶ below)\n\n    SetEnv ENV_DIR /var/lib/davis\n    # ... other env vars if needed\n</VirtualHost>\n```\n\n### Specific environment variables for IMAP and LDAP authentication methods\n\nIn case you use the `IMAP` auth type, you must specify the auth url (_the \"mailbox\" url_) in `IMAP_AUTH_URL` as `host:port`, the encryption method (SSL, TLS or None) and whether the certificate should be validated.\n\nYou should also explicitely define whether you want new authenticated users to be created upon login:\n\n```shell\nIMAP_AUTH_URL=imap.mydomain.com:993\nIMAP_ENCRYPTION_METHOD=ssl # ssl, tls or false\nIMAP_CERTIFICATE_VALIDATION=true\nIMAP_AUTH_USER_AUTOCREATE=true # false by default\n```\n\nSame goes for LDAP, where you must specify the LDAP server url, the DN pattern, the Mail attribute, as well as whether you want new authenticated users to be created upon login (_like for IMAP_):\n\n```shell\nLDAP_AUTH_URL=ldap://127.0.0.1:3890 # default LDAP port\nLDAP_DN_PATTERN=uid=%u,ou=users,dc=domain,dc=com\nLDAP_MAIL_ATTRIBUTE=mail\nLDAP_AUTH_USER_AUTOCREATE=true # false by default\nLDAP_CERTIFICATE_CHECKING_STRATEGY=try # try by default. Other values are: never, hard, demand or allow\n```\n\n> Ex: for [Zimbra LDAP](https://zimbra.github.io/adminguide/latest/#zimbra_ldap_service), you might want to use the `zimbraMailDeliveryAddress` attribute to retrieve the principal user email:\n>    ```shell\n>    LDAP_MAIL_ATTRIBUTE=zimbraMailDeliveryAddress\n>    ```\n\n## Migrating from Baïkal?\n\nIf you're migrating from Baïkal, then you will likely want to do the following :\n\n1. Get a backup of your data (without the `CREATE`  statements, but with complete `INSERT`  statements):\n\n    ```shell\n    mysqldump -u root -p --no-create-info --complete-insert baikal > baikal_to_davis.sql # baikal is the actual name of your database\n    ```\n\n2. Create a new database for Davis (let's name it `davis`) and create the base schema:\n\n    ```shell\n    bin/console doctrine:migrations:migrate 'DoctrineMigrations\\Version20191030113307' --no-interaction\n    ```\n\n3. Reimport the data back:\n\n    ```\n    mysql -uroot -p davis < baikal_to_davis.sql\n    ```\n\n4. Run the necessary remaining migrations:\n\n    ```\n    bin/console doctrine:migrations:migrate\n    ```\n\n> [!NOTE]\n> Some details / steps to resolve are also available in https://github.com/tchapi/davis/issues/226.\n\n# 🌐 Access / Webserver\n\nA simple status page is available on the root `/` of the server.\n\nThe administration interface is available at `/dashboard`. You need to login to use it (See `ADMIN_LOGIN` and `ADMIN_PASSWORD` env vars).\n\nThe main endpoint for CalDAV, WebDAV or CardDAV is at `/dav`.\n\n> [!TIP]\n>\n> For shared hosting, the `symfony/apache-pack` is included and provides a standard `.htaccess` file in the public directory so redirections should work out of the box.\n\n## API Endpoint\n\nFor user and calendar management there is an API endpoint. See [the API documentation](docs/api/README.md) for more information.\n\n> [!TIP]\n>\n> The API endpoint requires an environment variable `API_KEY` set to a secret key that you will use in the `X-Davis-API-Token` header of your requests to authenticate. You can generate it with `bin/console api:generate`\n\n## Webserver Configuration Examples\n\n### Example Caddy 2 configuration\n\n```\ndav.domain.tld {\n    # General settings\n    encode zstd gzip\n    header {\n        -Server\n        -X-Powered-By\n\n        # enable HSTS\n        Strict-Transport-Security max-age=31536000;\n\n        # disable clients from sniffing the media type\n        X-Content-Type-Options nosniff\n\n        # keep referrer data off of HTTP connections\n        Referrer-Policy no-referrer-when-downgrade\n    }\n\n    root * /var/www/davis/public\n    php_fastcgi 127.0.0.1:8000\n    file_server\n}\n```\n### Example Apache 2.4 configuration\n\n```apache\n<VirtualHost *:80>\n    ServerName dav.domain.tld\n\n    DocumentRoot /var/www/davis/public\n    DirectoryIndex /index.php\n\n    <Directory /var/www/davis/public>\n        AllowOverride None\n        Order Allow,Deny\n        Allow from All\n        FallbackResource /index.php\n    </Directory>\n\n    # Apache > 2.4.25, else remove this part\n    <Directory /var/www/davis/public/bundles>\n        FallbackResource disabled\n    </Directory>\n\n    # Env vars (if you did not use .env.local)\n    SetEnv APP_ENV prod\n    SetEnv APP_SECRET <app-secret-id>\n    SetEnv DATABASE_DRIVER \"mysql\"\n    SetEnv DATABASE_URL \"mysql://db_user:db_pass@host:3306/db_name?serverVersion=10.9.3-MariaDB&charset=utf8mb4\"\n    # ... etc\n</VirtualHost>\n```\n\n### Example Nginx configuration\n\n```nginx\nserver {\n    server_name dav.domain.tld;\n    root /var/www/davis/public;\n\n    location / {\n        try_files $uri /index.php$is_args$args;\n    }\n\n    location /bundles {\n        try_files $uri =404;\n    }\n\n    location ~ ^/index\\.php(/|$) {\n        fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; # Change for your PHP version\n        fastcgi_split_path_info ^(.+\\.php)(/.*)$;\n        include fastcgi_params;\n\n        # Env vars (if you did not use .env.local)\n        fastcgi_param APP_ENV prod;\n        fastcgi_param APP_SECRET <app-secret-id>;\n        fastcgi_param DATABASE_DRIVER \"mysql\";\n        fastcgi_param DATABASE_URL \"mysql://db_user:db_pass@host:3306/db_name?serverVersion=10.9.3-MariaDB&charset=utf8mb4\";\n        # ... etc ...\n\n        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n        fastcgi_param DOCUMENT_ROOT $realpath_root;\n        internal;\n    }\n\n    location ~ \\.php$ {\n        return 404;\n    }\n}\n```\n\nMore examples and information [here](https://symfony.com/doc/current/setup/web_server_configuration.html).\n\n## Well-known redirections for CalDAV and CardDAV\n\nWeb-based protocols like CalDAV and CardDAV can be found using a discovery service. Some clients require that you implement a path prefix to point to the correct location for your service. See [here](https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers) for more info.\n\nIf you use Apache as your webserver, you can enable the redirections with:\n\n```apache\nRewriteEngine On\nRewriteRule ^\\.well-known/carddav /dav/ [R=301,L]\nRewriteRule ^\\.well-known/caldav /dav/ [R=301,L]\n```\n\nMake sure that `mod_rewrite` is enabled on your installation beforehand.\n\nIf you use Nginx, you can add this to your configuration:\n\n```nginx\nlocation / {\n    rewrite ^/.well-known/carddav /dav/ redirect;\n    rewrite ^/.well-known/caldav /dav/ redirect;\n}\n```\n\n# 🐳 Dockerized installation\n\nA `Dockerfile` is available for you to compile the image.\n\nTo build the checked out version, just run:\n\n    docker build --pull --file docker/Dockerfile --tag davis:latest --build-arg fpm_user=82:82 .\n\n> [!TIP]\n>\n> The `fpm_user` build arg allows to set:\n>  - the uid FPM will run with\n>  - the owner of the app folder\n>\n> This is helpful if you have a proxy that does not use the same default PHP Alpine uid/gid for www-data (82:82). For instance, in the docker compose file, nginx uses 101:101\n>\n\nThis will build a `davis:latest` image that you can directly use. Do not forget to pass sensible environment variables to the container since the _dist_ `.env` file will take precedence if no `.env.local` or environment variable is found.\n\nYou can use `--platform` to specify the platform to build for. Currently, `arm64` (ARMv8) and `amd64` (x86) are supported.\n\n> [!IMPORTANT]\n>\n> ⚠ Do not forget to run all the database migrations the first time you run the container :\n>\n>     docker exec -it davis sh -c \"APP_ENV=prod bin/console doctrine:migrations:migrate --no-interaction\"\n\n## Docker images\n\nFor each release, a Docker image is built and published in the [Github package repository](https://github.com/tchapi/davis/pkgs/container/davis).\n\n### Release images\n\nEach release builds and tags two images: one for the standard build (no reverse-proxy) and one for the standalone build (including Caddy as a reverse-proxy). Example:\n\n```\ndocker pull ghcr.io/tchapi/davis:v4.4.0\n```\n\n```\ndocker pull ghcr.io/tchapi/davis-standalone:v4.4.0\n```\n\n### Edge image\n\nThe edge image is generally built from the tip of the main branch, but might sometimes be used for specific branch testing:\n\n```\ndocker pull ghcr.io/tchapi/davis:edge\n```\n\n> [!WARNING]\n>\n> The `edge` image must not be considered stable. **Use only release images for production setups**.\n\n## Full stack\n\nA few `docker-compose.yml` files are also included (in the `docker` folder) as minimal example setups, with various databases for instance.\n\nYou can start the containers with :\n\n    cd docker && docker compose up -d\n\n> [!NOTE]\n>\n> The default recipe above uses MariaDB.\n\n> [!IMPORTANT]\n>\n> ⚠ Do not forget to run all the database migrations the first time you run the container :\n>\n>     docker exec -it davis sh -c \"APP_ENV=prod bin/console doctrine:migrations:migrate --no-interaction\"\n\n> [!WARNING]\n>\n> For SQLite, you must also make sure that the folder the database will reside in AND the database file in itself have the right permissions! You can do for instance:\n> `chown -R www-data: /data` if `/data` is the folder your SQLite database will be in, just after you have run the migrations\n\n### Updating from a previous version\n\nIf you update the code, you need to make sure the database structure is in sync.\n\n**Before v3.0.0**, you need to force the update:\n\n    docker exec -it davis sh -c \"APP_ENV=prod bin/console doctrine:schema:update --force --no-interaction\"\n\n**For v3.0.0 and after**, you can just migrate again (_provided you correctly followed the migration notes in the v3.0.0 release_):\n\n    docker exec -it davis sh -c \"APP_ENV=prod bin/console doctrine:migrations:migrate --no-interaction\"\n\n\nThen, head up to `http://<YOUR_DOCKER_IP>:9000` to see the status display :\n\n![Status page](_screenshots/status.png)\n\n> Note that there is no user and no principals created by default.\n\n# NixOS Installation\n\nTo install Davis on NixOS, you can use the builtin NixOS module [`services.davis`](https://search.nixos.org/options?channel=unstable&query=services.davis).\n\nCurrently the NixOS module and package are in the nixos-unstable channel, but they are slated to enter the stable channel in the 24.05 release.\n\n* [All `services.davis` options](https://search.nixos.org/options?channel=unstable&query=services.davis)\n* [Basic Guide](https://nixos.org/manual/nixos/unstable/#module-services-davis)\n\nIf you encounter a bug or problem with the NixOS Davis module please open an issue [at the nixpkgs repo](https://github.com/NixOS/nixpkgs/issues/new/choose) so the module maintainers can assist.\n\n# Development\n\nYou can spin off a local PHP webserver with:\n\n    php -S localhost:8000 -t public\n\nIf you change or add translations, you need to update the `messages` XLIFF file with:\n\n    bin/console translation:extract en --force --domain=messages+intl-icu\n\n## Testing\n\nYou can use:\n\n    ./vendor/bin/phpunit\n\n## ✨ Code linting\n\nWe use [PHP-CS-Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) with:\n\n    PHP_CS_FIXER_IGNORE_ENV=True ./vendor/bin/php-cs-fixer fix\n\n## ❓ How-to's\n\nBelow are some issues that can bring more info / insight into custom setups that Davis users have experienced in the past. Hopefully it can help:\n\n  - **Davis on Proxmox / TrueNAS Scale**: https://github.com/tchapi/davis/issues/164\n\n\n## 🐛 Troubleshooting\n\nDepending on how you run Davis, logs are either:\n  - [dev] printed out directly in the console\n  - [dev] available in the Symfony Debug Bar in the [Profiler](https://symfony.com/doc/current/profiler.html)\n  - [dev] logged in `./var/log/dev.log`\n  - [prod] logged in `./var/log/prod.log`, but only if there has been an error (_it's the fingers_crossed filter, explained [here](https://symfony.com/doc/current/logging.html#handlers-that-modify-log-entries)_)\n\n> [!NOTE]\n>\n> It's `./var/log` (relative to the Davis installation), not `/var/log`.\n>\n> To tail the aplication log on Docker, do:\n> ```\n> docker exec -it davis tail /var/www/davis/var/log/prod.log\n> ```\n\n### I have a \"Bad timezone configuration env var\" error on the dashboard\n\nIf you see this:\n\n![Bad timezone configuration env var error](_screenshots/bad_timezone_configuration_env_var.png)\n\nIt means that the value you set for the `APP_TIMEZONE` env var is not a correct timezone, as per [the official list](https://www.php.net/manual/en/timezones.php). Your timezone has thus not been set and is the server's default (Here, UTC). Adjust the setting accordingly.\n\n### I have a 500 and no tables have been created\n\nYou probably forgot to run the migration once to create the necessary DB schema\n\nIn Docker:\n\n```shell\ndocker exec -it davis sh -c \"APP_ENV=prod bin/console doctrine:migrations:migrate --no-interaction\"\n```\n\nIn a shell, if you run Davis locally:\n\n    bin/console doctrine:migrations:migrate\n\n### I have a 500 and a log about `Uncaught Error: Class \"Symfony\\Bundle\\WebProfilerBundle\\WebProfilerBundle\" not found`\n\nYou are running the app in dev mode, but you haven't installed the dev dependencies. Either:\n\na. Set `APP_ENV=prod` in your local env file (See configuration above)\n\nb. Or `composer install` (without the `--no-dev` flag)\n\n\n### The LDAP connection is not working\n\n> [!NOTE]\n>\n> Make sure all environment parameters are in plain text (no quotes).\n\nCheck if your instance can reach your LDAP server:\n\n  - For Docker instances: make sure it is on the same network\n  - Check connection via `ldapsearch`:\n\n    ```shell\n    # For docker: connect into container's shell\n    docker exec -it davis sh\n\n    # install ldap utils (for alpine linux)\n    apk add openldap-clients\n\n    # User checking their own entry\n    ldapsearch -H ldap://lldap-server:3890 -D \"uid=someuser,ou=users,dc=domain,dc=com\" -W -b \"dc=domain,dc=com\" \"(uid=someuser)\"\n    ```\n\n  - Check that the `LDAP_DN_PATTERN` filter is compliant with your LDAP service\n  - Example: `uid=%u,ou=people,dc=domain,dc=com`: [LLDAP](https://github.com/lldap/lldap) uses `people` instead of `users`.\n\n### The birthday calendar is not synced / not up to date\n\nAn update event might have been missed. In this case, it's easy to resync all contacts by issuing the command:\n\n```\nbin/console dav:sync-birthday-calendar\n```\n\n# 📚 Libraries used\n\n  - Symfony 7 (Licence : MIT)\n  - Sabre-io/dav (Licence : BSD-3-Clause)\n  - Bootstrap 5 (Licence : MIT)\n\n_This project does not use any pipeline for the assets since the frontend side is relatively simple, and based on Bootstrap._\n\n# ⚖️ Licence\n\nThis project is release under the MIT licence. See the LICENCE file\n\n[ci_badge]: https://github.com/tchapi/davis/workflows/CI/badge.svg\n[ci_link]: https://github.com/tchapi/davis/actions?query=workflow%3ACI\n\n[sponsor_badge]: https://img.shields.io/badge/sponsor%20me-🙏-blue?logo=paypal\n[sponsor_link]: https://paypal.me/tchap\n\n[release_badge]: https://img.shields.io/github/v/release/tchapi/davis\n[release_link]: https://github.com/tchapi/davis/releases\n"
  },
  {
    "path": "bin/console",
    "content": "#!/usr/bin/env php\n<?php\n\n$overridenEnvDir = getenv('ENV_DIR') ?: null;\n\nif ($overridenEnvDir) {\n    // Tell the Runtime not to touch dotenv so we can load our own file.\n    // This is needed if the ENV_DIR is outside of the project directory\n    $_SERVER['APP_RUNTIME_OPTIONS']['disable_dotenv'] = true;\n}\n\nuse App\\Kernel;\nuse Symfony\\Bundle\\FrameworkBundle\\Console\\Application;\nuse Symfony\\Component\\Dotenv\\Dotenv;\n\nif (!is_dir(dirname(__DIR__).'/vendor')) {\n    throw new LogicException('Dependencies are missing. Try running \"composer install\".');\n}\n\nif (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {\n    throw new LogicException('Symfony Runtime is missing. Try running \"composer require symfony/runtime\".');\n}\n\nrequire_once dirname(__DIR__).'/vendor/autoload_runtime.php';\n\nif ($overridenEnvDir) {\n    // Load our own now, after the runtime has booted\n    (new Dotenv())->bootEnv($overridenEnvDir.'/.env');\n}\n\nreturn function (array $context) {\n    $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);\n\n    return new Application($kernel);\n};\n"
  },
  {
    "path": "bin/phpunit",
    "content": "#!/usr/bin/env php\n<?php\n\nif (!ini_get('date.timezone')) {\n    ini_set('date.timezone', 'UTC');\n}\n\nif (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {\n    define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');\n    require PHPUNIT_COMPOSER_INSTALL;\n    PHPUnit\\TextUI\\Command::main();\n} else {\n    if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {\n        echo \"Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\\n\";\n        exit(1);\n    }\n\n    require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';\n}\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"tchapi/davis\",\n    \"description\": \"A simple, fully translatable admin interface and frontend for sabre/dav based on Symfony\",\n    \"type\": \"project\",\n    \"license\": \"MIT\",\n    \"require\": {\n        \"php\": \"^8.2\",\n        \"ext-ctype\": \"*\",\n        \"ext-gd\": \"*\",\n        \"ext-iconv\": \"*\",\n        \"ext-zip\": \"*\",\n        \"composer-runtime-api\": \"^2\",\n        \"dantsu/php-osm-static-api\": \"^0.6.4\",\n        \"doctrine/doctrine-bundle\": \"^2.15.1\",\n        \"doctrine/doctrine-migrations-bundle\": \"^3.4.2\",\n        \"doctrine/orm\": \"^2.20.6\",\n        \"sabre/dav\": \"^4.7.0\",\n        \"symfony/apache-pack\": \"^1.0.1\",\n        \"symfony/asset\": \"^7.4\",\n        \"symfony/console\": \"^7.4\",\n        \"symfony/dotenv\": \"^7.4\",\n        \"symfony/expression-language\": \"^7.4\",\n        \"symfony/flex\": \"^2.7.1\",\n        \"symfony/form\": \"^7.4\",\n        \"symfony/framework-bundle\": \"^7.4\",\n        \"symfony/http-client\": \"^7.4\",\n        \"symfony/intl\": \"^7.4\",\n        \"symfony/mailer\": \"^7.4\",\n        \"symfony/monolog-bundle\": \"^3.10.0\",\n        \"symfony/polyfill-intl-messageformatter\": \"^1.31\",\n        \"symfony/process\": \"^7.4\",\n        \"symfony/property-access\": \"^7.4\",\n        \"symfony/property-info\": \"^7.4\",\n        \"symfony/runtime\": \"^7.4\",\n        \"symfony/security-bundle\": \"^7.4\",\n        \"symfony/serializer\": \"^7.4\",\n        \"symfony/translation\": \"^7.4\",\n        \"symfony/twig-bundle\": \"^7.4\",\n        \"symfony/validator\": \"^7.4\",\n        \"symfony/web-link\": \"^7.4\",\n        \"symfony/yaml\": \"^7.4\",\n        \"webklex/php-imap\": \"^6.2\"\n    },\n    \"require-dev\": {\n        \"doctrine/doctrine-fixtures-bundle\": \"^3.5\",\n        \"friendsofphp/php-cs-fixer\": \"^3.49.0\",\n        \"phpunit/phpunit\": \"^10.5.10\",\n        \"symfony/browser-kit\": \"^7.4\",\n        \"symfony/css-selector\": \"^7.4\",\n        \"symfony/debug-bundle\": \"^7.4\",\n        \"symfony/maker-bundle\": \"^1.54\",\n        \"symfony/phpunit-bridge\": \"^7.4\",\n        \"symfony/stopwatch\": \"^7.4\",\n        \"symfony/web-profiler-bundle\": \"^7.4\"\n    },\n    \"config\": {\n        \"preferred-install\": {\n            \"*\": \"dist\"\n        },\n        \"sort-packages\": true,\n        \"platform\": {\n            \"php\": \"8.2.15\"\n        },\n        \"allow-plugins\": {\n            \"composer/package-versions-deprecated\": true,\n            \"symfony/flex\": true,\n            \"symfony/runtime\": true\n        }\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"App\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"App\\\\Tests\\\\\": \"tests/\"\n        }\n    },\n    \"scripts\": {\n        \"auto-scripts\": {\n            \"cache:clear\": \"symfony-cmd\",\n            \"assets:install %PUBLIC_DIR%\": \"symfony-cmd\"\n        },\n        \"post-install-cmd\": [\n            \"@auto-scripts\"\n        ],\n        \"post-update-cmd\": [\n            \"@auto-scripts\"\n        ]\n    },\n    \"conflict\": {\n        \"symfony/symfony\": \"*\"\n    },\n    \"extra\": {\n        \"symfony\": {\n            \"allow-contrib\": false,\n            \"require\": \"7.4.*\"\n        }\n    }\n}\n"
  },
  {
    "path": "config/bundles.php",
    "content": "<?php\n\nreturn [\n    Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle::class => ['all' => true],\n    Doctrine\\Bundle\\DoctrineBundle\\DoctrineBundle::class => ['all' => true],\n    Doctrine\\Bundle\\MigrationsBundle\\DoctrineMigrationsBundle::class => ['all' => true],\n    Symfony\\Bundle\\SecurityBundle\\SecurityBundle::class => ['all' => true],\n    Symfony\\Bundle\\TwigBundle\\TwigBundle::class => ['all' => true],\n    Symfony\\Bundle\\WebProfilerBundle\\WebProfilerBundle::class => ['dev' => true, 'test' => true],\n    Symfony\\Bundle\\MonologBundle\\MonologBundle::class => ['all' => true],\n    Symfony\\Bundle\\DebugBundle\\DebugBundle::class => ['dev' => true, 'test' => true],\n    Symfony\\Bundle\\MakerBundle\\MakerBundle::class => ['dev' => true],\n    Doctrine\\Bundle\\FixturesBundle\\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],\n];\n"
  },
  {
    "path": "config/packages/cache.yaml",
    "content": "framework:\n    cache:\n        # Unique name of your app: used to compute stable namespaces for cache keys.\n        #prefix_seed: your_vendor_name/app_name\n\n        # The \"app\" cache stores to the filesystem by default.\n        # The data in this cache should persist between deploys.\n        # Other options include:\n\n        # Redis\n        #app: cache.adapter.redis\n        #default_redis_provider: redis://localhost\n\n        # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)\n        #app: cache.adapter.apcu\n\n        # Namespaced pools use the above \"app\" backend by default\n        #pools:\n            #my.dedicated.cache: null\n"
  },
  {
    "path": "config/packages/dev/debug.yaml",
    "content": "debug:\n    # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.\n    # See the \"server:dump\" command to start a new server.\n    dump_destination: \"tcp://%env(VAR_DUMPER_SERVER)%\"\n"
  },
  {
    "path": "config/packages/dev/monolog.yaml",
    "content": "monolog:\n    handlers:\n        main:\n            type: stream\n            path: \"%kernel.logs_dir%/%kernel.environment%.log\"\n            level: debug\n            channels: [\"!event\"]\n        # uncomment to get logging in your browser\n        # you may have to allow bigger header sizes in your Web server configuration\n        #firephp:\n        #    type: firephp\n        #    level: info\n        #chromephp:\n        #    type: chromephp\n        #    level: info\n        console:\n            type: console\n            process_psr_3_messages: false\n            channels: [\"!event\", \"!doctrine\", \"!console\"]\n"
  },
  {
    "path": "config/packages/dev/web_profiler.yaml",
    "content": "web_profiler:\n    toolbar: true\n    intercept_redirects: false\n\nframework:\n    profiler: { only_exceptions: false }\n"
  },
  {
    "path": "config/packages/doctrine.yaml",
    "content": "doctrine:\n    dbal:\n        # The server_version must be configured directly in the\n        # DATABASE_URL to allow different drivers without adding\n        # too many env vars\n        driver: 'pdo_%env(string:default:default_database_driver:DATABASE_DRIVER)%'\n\n        url: '%env(resolve:DATABASE_URL)%'\n\n    orm:\n        auto_generate_proxy_classes: true\n        report_fields_where_declared: true\n        enable_lazy_ghost_objects: true\n        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware\n        controller_resolver:\n            auto_mapping: false\n        mappings:\n            App:\n                is_bundle: false\n                type: attribute\n                dir: '%kernel.project_dir%/src/Entity'\n                prefix: 'App\\Entity'\n                alias: App\n"
  },
  {
    "path": "config/packages/doctrine_migrations.yaml",
    "content": "doctrine_migrations:\n    migrations_paths:\n        # namespace is arbitrary but should be different from App\\Migrations\n        # as migrations classes should NOT be autoloaded\n        'DoctrineMigrations': '%kernel.project_dir%/migrations'\n"
  },
  {
    "path": "config/packages/framework.yaml",
    "content": "# see https://symfony.com/doc/current/reference/configuration/framework.html\nframework:\n    secret: '%env(APP_SECRET)%'\n    handle_all_throwables: true\n    #csrf_protection: true\n    http_method_override: false\n\n    # Enables session support. Note that the session will ONLY be started if you read or write from it.\n    # Remove or comment this section to explicitly disable session support.\n    session:\n        handler_id: null\n        cookie_secure: auto\n        cookie_samesite: lax\n        name: 'DAVIS_SESSION'\n        storage_factory_id: session.storage.factory.native\n\n    profiler:\n        collect_serializer_data: true\n\n    property_info:\n        with_constructor_extractor: false\n\n    php_errors:\n        log: true\n"
  },
  {
    "path": "config/packages/mailer.yaml",
    "content": "framework:\n    mailer:\n        dsn: '%env(MAILER_DSN)%'\n"
  },
  {
    "path": "config/packages/prod/deprecations.yaml",
    "content": "# As of Symfony 5.1, deprecations are logged in the dedicated \"deprecation\" channel when it exists\n#monolog:\n#    channels: [deprecation]\n#    handlers:\n#        deprecation:\n#            type: stream\n#            channels: [deprecation]\n#            path: \"%kernel.logs_dir%/%kernel.environment%.deprecations.log\"\n"
  },
  {
    "path": "config/packages/prod/doctrine.yaml",
    "content": "doctrine:\n    orm:\n        auto_generate_proxy_classes: false\n        metadata_cache_driver:\n            type: pool\n            pool: doctrine.system_cache_pool\n        query_cache_driver:\n            type: pool\n            pool: doctrine.system_cache_pool\n        result_cache_driver:\n            type: pool\n            pool: doctrine.result_cache_pool\n\nframework:\n    cache:\n        pools:\n            doctrine.result_cache_pool:\n                adapter: cache.app\n            doctrine.system_cache_pool:\n                adapter: cache.system\n"
  },
  {
    "path": "config/packages/prod/monolog.yaml",
    "content": "monolog:\n    handlers:\n        main:\n            type: fingers_crossed\n            action_level: error\n            handler: nested\n            excluded_http_codes: [404, 405]\n            buffer_size: 50 # How many messages should be saved? Prevent memory leaks\n        nested:\n            type: stream\n            path: \"%env(resolve:LOG_FILE_PATH)%\"\n            level: debug\n        console:\n            type: console\n            process_psr_3_messages: false\n            channels: [\"!event\", \"!doctrine\"]\n"
  },
  {
    "path": "config/packages/prod/routing.yaml",
    "content": "framework:\n    router:\n        strict_requirements: null\n"
  },
  {
    "path": "config/packages/routing.yaml",
    "content": "framework:\n    router:\n        utf8: true\n\n        # Configure how to generate URLs in non-HTTP contexts, such as CLI commands.\n        # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands\n        #default_uri: http://localhost\n"
  },
  {
    "path": "config/packages/security.yaml",
    "content": "security:\n    password_hashers:\n        Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface: 'auto'\n    providers:\n        admin_user_provider:\n            id: App\\Security\\AdminUserProvider\n    firewalls:\n        dev:\n            pattern: ^/(_(profiler|wdt)|css|images|js)/\n            security: false\n        api_v1:\n            pattern: ^/api/v1\n            stateless: true\n            custom_authenticators:\n                - App\\Security\\ApiKeyAuthenticator\n        main:\n            lazy: true\n            custom_authenticators:\n                - App\\Security\\LoginFormAuthenticator\n            provider: admin_user_provider\n            logout:\n                path: app_logout\n                target: dashboard\n            \n\n    access_control:\n        - { path: ^/$, roles: PUBLIC_ACCESS }\n        - { path: ^/dav, roles: PUBLIC_ACCESS }\n        - { path: ^/dashboard, roles: ROLE_ADMIN, allow_if: \"'%env(default:default_admin_auth_bypass:ADMIN_AUTH_BYPASS)%' === 'true'\" }\n        - { path: ^/users, roles: ROLE_ADMIN, allow_if: \"'%env(default:default_admin_auth_bypass:ADMIN_AUTH_BYPASS)%' === 'true'\" }\n        - { path: ^/calendars, roles: ROLE_ADMIN, allow_if: \"'%env(default:default_admin_auth_bypass:ADMIN_AUTH_BYPASS)%' === 'true'\" }\n        - { path: ^/adressbooks, roles: ROLE_ADMIN, allow_if: \"'%env(default:default_admin_auth_bypass:ADMIN_AUTH_BYPASS)%' === 'true'\" }\n        - { path: ^/api/v1/health$, roles: PUBLIC_ACCESS }\n        - { path: ^/api, roles: IS_AUTHENTICATED }\n"
  },
  {
    "path": "config/packages/test/framework.yaml",
    "content": "framework:\n    test: true\n    session:\n        storage_factory_id: session.storage.factory.mock_file"
  },
  {
    "path": "config/packages/test/monolog.yaml",
    "content": "monolog:\n    handlers:\n        main:\n            type: fingers_crossed\n            action_level: error\n            handler: nested\n            excluded_http_codes: [404, 405]\n            channels: [\"!event\"]\n        nested:\n            type: stream\n            path: \"%kernel.logs_dir%/%kernel.environment%.log\"\n            level: debug\n"
  },
  {
    "path": "config/packages/test/twig.yaml",
    "content": "twig:\n    strict_variables: true\n"
  },
  {
    "path": "config/packages/test/validator.yaml",
    "content": "framework:\n    validation:\n        not_compromised_password: false\n"
  },
  {
    "path": "config/packages/test/web_profiler.yaml",
    "content": "web_profiler:\n    toolbar: false\n    intercept_redirects: false\n\nframework:\n    profiler: { collect: false }\n"
  },
  {
    "path": "config/packages/translation.yaml",
    "content": "framework:\n    default_locale: en\n    translator:\n        default_path: '%kernel.project_dir%/translations'\n        fallbacks:\n            - en\n"
  },
  {
    "path": "config/packages/twig.yaml",
    "content": "twig:\n    default_path: '%kernel.project_dir%/templates'\n    debug: '%kernel.debug%'\n    strict_variables: '%kernel.debug%'\n    form_themes: ['bootstrap_5_horizontal_layout.html.twig']\n    globals:\n        invite_from_address: '%env(INVITE_FROM_ADDRESS)%'\n        calDAVEnabled: '%env(bool:CALDAV_ENABLED)%'\n        cardDAVEnabled: '%env(bool:CARDDAV_ENABLED)%'\n        webDAVEnabled: '%env(bool:WEBDAV_ENABLED)%'\n        authRealm: '%env(AUTH_REALM)%'\n        authMethod: '%env(AUTH_METHOD)%'\n"
  },
  {
    "path": "config/packages/validator.yaml",
    "content": "framework:\n    validation:\n        enable_attributes: true\n        email_validation_mode: html5\n\n        # Enables validator auto-mapping support.\n        # For instance, basic validation constraints will be inferred from Doctrine's metadata.\n        #auto_mapping:\n        #    App\\Entity\\: []\n"
  },
  {
    "path": "config/reference.php",
    "content": "<?php\n\n// This file is auto-generated and is for apps only. Bundles SHOULD NOT rely on its content.\n\nnamespace Symfony\\Component\\DependencyInjection\\Loader\\Configurator;\n\nuse Symfony\\Component\\Config\\Loader\\ParamConfigurator as Param;\n\n/**\n * This class provides array-shapes for configuring the services and bundles of an application.\n *\n * Services declared with the config() method below are autowired and autoconfigured by default.\n *\n * This is for apps only. Bundles SHOULD NOT use it.\n *\n * Example:\n *\n *     ```php\n *     // config/services.php\n *     namespace Symfony\\Component\\DependencyInjection\\Loader\\Configurator;\n *\n *     return App::config([\n *         'services' => [\n *             'App\\\\' => [\n *                 'resource' => '../src/',\n *             ],\n *         ],\n *     ]);\n *     ```\n *\n * @psalm-type ImportsConfig = list<string|array{\n *     resource: string,\n *     type?: string|null,\n *     ignore_errors?: bool,\n * }>\n * @psalm-type ParametersConfig = array<string, scalar|\\UnitEnum|array<scalar|\\UnitEnum|array<mixed>|Param|null>|Param|null>\n * @psalm-type ArgumentsType = list<mixed>|array<string, mixed>\n * @psalm-type CallType = array<string, ArgumentsType>|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool}\n * @psalm-type TagsType = list<string|array<string, array<string, mixed>>> // arrays inside the list must have only one element, with the tag name as the key\n * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\\Closure|ReferenceConfigurator|ExpressionConfigurator\n * @psalm-type DeprecationType = array{package: string, version: string, message?: string}\n * @psalm-type DefaultsType = array{\n *     public?: bool,\n *     tags?: TagsType,\n *     resource_tags?: TagsType,\n *     autowire?: bool,\n *     autoconfigure?: bool,\n *     bind?: array<string, mixed>,\n * }\n * @psalm-type InstanceofType = array{\n *     shared?: bool,\n *     lazy?: bool|string,\n *     public?: bool,\n *     properties?: array<string, mixed>,\n *     configurator?: CallbackType,\n *     calls?: list<CallType>,\n *     tags?: TagsType,\n *     resource_tags?: TagsType,\n *     autowire?: bool,\n *     bind?: array<string, mixed>,\n *     constructor?: string,\n * }\n * @psalm-type DefinitionType = array{\n *     class?: string,\n *     file?: string,\n *     parent?: string,\n *     shared?: bool,\n *     synthetic?: bool,\n *     lazy?: bool|string,\n *     public?: bool,\n *     abstract?: bool,\n *     deprecated?: DeprecationType,\n *     factory?: CallbackType,\n *     configurator?: CallbackType,\n *     arguments?: ArgumentsType,\n *     properties?: array<string, mixed>,\n *     calls?: list<CallType>,\n *     tags?: TagsType,\n *     resource_tags?: TagsType,\n *     decorates?: string,\n *     decoration_inner_name?: string,\n *     decoration_priority?: int,\n *     decoration_on_invalid?: 'exception'|'ignore'|null,\n *     autowire?: bool,\n *     autoconfigure?: bool,\n *     bind?: array<string, mixed>,\n *     constructor?: string,\n *     from_callable?: CallbackType,\n * }\n * @psalm-type AliasType = string|array{\n *     alias: string,\n *     public?: bool,\n *     deprecated?: DeprecationType,\n * }\n * @psalm-type PrototypeType = array{\n *     resource: string,\n *     namespace?: string,\n *     exclude?: string|list<string>,\n *     parent?: string,\n *     shared?: bool,\n *     lazy?: bool|string,\n *     public?: bool,\n *     abstract?: bool,\n *     deprecated?: DeprecationType,\n *     factory?: CallbackType,\n *     arguments?: ArgumentsType,\n *     properties?: array<string, mixed>,\n *     configurator?: CallbackType,\n *     calls?: list<CallType>,\n *     tags?: TagsType,\n *     resource_tags?: TagsType,\n *     autowire?: bool,\n *     autoconfigure?: bool,\n *     bind?: array<string, mixed>,\n *     constructor?: string,\n * }\n * @psalm-type StackType = array{\n *     stack: list<DefinitionType|AliasType|PrototypeType|array<class-string, ArgumentsType|null>>,\n *     public?: bool,\n *     deprecated?: DeprecationType,\n * }\n * @psalm-type ServicesConfig = array{\n *     _defaults?: DefaultsType,\n *     _instanceof?: InstanceofType,\n *     ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null>\n * }\n * @psalm-type ExtensionType = array<string, mixed>\n * @psalm-type FrameworkConfig = array{\n *     secret?: scalar|Param|null,\n *     http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false\n *     allowed_http_method_override?: list<string|Param>|null,\n *     trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: \"%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%\"\n *     ide?: scalar|Param|null, // Default: \"%env(default::SYMFONY_IDE)%\"\n *     test?: bool|Param,\n *     default_locale?: scalar|Param|null, // Default: \"en\"\n *     set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the \"_locale\" request attribute is not passed). // Default: false\n *     set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false\n *     enabled_locales?: list<scalar|Param|null>,\n *     trusted_hosts?: list<scalar|Param|null>,\n *     trusted_proxies?: mixed, // Default: [\"%env(default::SYMFONY_TRUSTED_PROXIES)%\"]\n *     trusted_headers?: list<scalar|Param|null>,\n *     error_controller?: scalar|Param|null, // Default: \"error_controller\"\n *     handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \\Throwable. // Default: true\n *     csrf_protection?: bool|array{\n *         enabled?: scalar|Param|null, // Default: null\n *         stateless_token_ids?: list<scalar|Param|null>,\n *         check_header?: scalar|Param|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false\n *         cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: \"csrf-token\"\n *     },\n *     form?: bool|array{ // Form configuration\n *         enabled?: bool|Param, // Default: true\n *         csrf_protection?: bool|array{\n *             enabled?: scalar|Param|null, // Default: null\n *             token_id?: scalar|Param|null, // Default: null\n *             field_name?: scalar|Param|null, // Default: \"_token\"\n *             field_attr?: array<string, scalar|Param|null>,\n *         },\n *     },\n *     http_cache?: bool|array{ // HTTP cache configuration\n *         enabled?: bool|Param, // Default: false\n *         debug?: bool|Param, // Default: \"%kernel.debug%\"\n *         trace_level?: \"none\"|\"short\"|\"full\"|Param,\n *         trace_header?: scalar|Param|null,\n *         default_ttl?: int|Param,\n *         private_headers?: list<scalar|Param|null>,\n *         skip_response_headers?: list<scalar|Param|null>,\n *         allow_reload?: bool|Param,\n *         allow_revalidate?: bool|Param,\n *         stale_while_revalidate?: int|Param,\n *         stale_if_error?: int|Param,\n *         terminate_on_cache_hit?: bool|Param,\n *     },\n *     esi?: bool|array{ // ESI configuration\n *         enabled?: bool|Param, // Default: false\n *     },\n *     ssi?: bool|array{ // SSI configuration\n *         enabled?: bool|Param, // Default: false\n *     },\n *     fragments?: bool|array{ // Fragments configuration\n *         enabled?: bool|Param, // Default: false\n *         hinclude_default_template?: scalar|Param|null, // Default: null\n *         path?: scalar|Param|null, // Default: \"/_fragment\"\n *     },\n *     profiler?: bool|array{ // Profiler configuration\n *         enabled?: bool|Param, // Default: false\n *         collect?: bool|Param, // Default: true\n *         collect_parameter?: scalar|Param|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null\n *         only_exceptions?: bool|Param, // Default: false\n *         only_main_requests?: bool|Param, // Default: false\n *         dsn?: scalar|Param|null, // Default: \"file:%kernel.cache_dir%/profiler\"\n *         collect_serializer_data?: bool|Param, // Enables the serializer data collector and profiler panel. // Default: false\n *     },\n *     workflows?: bool|array{\n *         enabled?: bool|Param, // Default: false\n *         workflows?: array<string, array{ // Default: []\n *             audit_trail?: bool|array{\n *                 enabled?: bool|Param, // Default: false\n *             },\n *             type?: \"workflow\"|\"state_machine\"|Param, // Default: \"state_machine\"\n *             marking_store?: array{\n *                 type?: \"method\"|Param,\n *                 property?: scalar|Param|null,\n *                 service?: scalar|Param|null,\n *             },\n *             supports?: list<scalar|Param|null>,\n *             definition_validators?: list<scalar|Param|null>,\n *             support_strategy?: scalar|Param|null,\n *             initial_marking?: list<scalar|Param|null>,\n *             events_to_dispatch?: list<string|Param>|null,\n *             places?: list<array{ // Default: []\n *                 name?: scalar|Param|null,\n *                 metadata?: array<string, mixed>,\n *             }>,\n *             transitions?: list<array{ // Default: []\n *                 name?: string|Param,\n *                 guard?: string|Param, // An expression to block the transition.\n *                 from?: list<array{ // Default: []\n *                     place?: string|Param,\n *                     weight?: int|Param, // Default: 1\n *                 }>,\n *                 to?: list<array{ // Default: []\n *                     place?: string|Param,\n *                     weight?: int|Param, // Default: 1\n *                 }>,\n *                 weight?: int|Param, // Default: 1\n *                 metadata?: array<string, mixed>,\n *             }>,\n *             metadata?: array<string, mixed>,\n *         }>,\n *     },\n *     router?: bool|array{ // Router configuration\n *         enabled?: bool|Param, // Default: false\n *         resource?: scalar|Param|null,\n *         type?: scalar|Param|null,\n *         cache_dir?: scalar|Param|null, // Deprecated: Setting the \"framework.router.cache_dir.cache_dir\" configuration option is deprecated. It will be removed in version 8.0. // Default: \"%kernel.build_dir%\"\n *         default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null\n *         http_port?: scalar|Param|null, // Default: 80\n *         https_port?: scalar|Param|null, // Default: 443\n *         strict_requirements?: scalar|Param|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true\n *         utf8?: bool|Param, // Default: true\n *     },\n *     session?: bool|array{ // Session configuration\n *         enabled?: bool|Param, // Default: false\n *         storage_factory_id?: scalar|Param|null, // Default: \"session.storage.factory.native\"\n *         handler_id?: scalar|Param|null, // Defaults to using the native session handler, or to the native *file* session handler if \"save_path\" is not null.\n *         name?: scalar|Param|null,\n *         cookie_lifetime?: scalar|Param|null,\n *         cookie_path?: scalar|Param|null,\n *         cookie_domain?: scalar|Param|null,\n *         cookie_secure?: true|false|\"auto\"|Param, // Default: \"auto\"\n *         cookie_httponly?: bool|Param, // Default: true\n *         cookie_samesite?: null|\"lax\"|\"strict\"|\"none\"|Param, // Default: \"lax\"\n *         use_cookies?: bool|Param,\n *         gc_divisor?: scalar|Param|null,\n *         gc_probability?: scalar|Param|null,\n *         gc_maxlifetime?: scalar|Param|null,\n *         save_path?: scalar|Param|null, // Defaults to \"%kernel.cache_dir%/sessions\" if the \"handler_id\" option is not null.\n *         metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0\n *         sid_length?: int|Param, // Deprecated: Setting the \"framework.session.sid_length.sid_length\" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option.\n *         sid_bits_per_character?: int|Param, // Deprecated: Setting the \"framework.session.sid_bits_per_character.sid_bits_per_character\" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option.\n *     },\n *     request?: bool|array{ // Request configuration\n *         enabled?: bool|Param, // Default: false\n *         formats?: array<string, string|list<scalar|Param|null>>,\n *     },\n *     assets?: bool|array{ // Assets configuration\n *         enabled?: bool|Param, // Default: true\n *         strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false\n *         version_strategy?: scalar|Param|null, // Default: null\n *         version?: scalar|Param|null, // Default: null\n *         version_format?: scalar|Param|null, // Default: \"%%s?%%s\"\n *         json_manifest_path?: scalar|Param|null, // Default: null\n *         base_path?: scalar|Param|null, // Default: \"\"\n *         base_urls?: list<scalar|Param|null>,\n *         packages?: array<string, array{ // Default: []\n *             strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false\n *             version_strategy?: scalar|Param|null, // Default: null\n *             version?: scalar|Param|null,\n *             version_format?: scalar|Param|null, // Default: null\n *             json_manifest_path?: scalar|Param|null, // Default: null\n *             base_path?: scalar|Param|null, // Default: \"\"\n *             base_urls?: list<scalar|Param|null>,\n *         }>,\n *     },\n *     asset_mapper?: bool|array{ // Asset Mapper configuration\n *         enabled?: bool|Param, // Default: false\n *         paths?: array<string, scalar|Param|null>,\n *         excluded_patterns?: list<scalar|Param|null>,\n *         exclude_dotfiles?: bool|Param, // If true, any files starting with \".\" will be excluded from the asset mapper. // Default: true\n *         server?: bool|Param, // If true, a \"dev server\" will return the assets from the public directory (true in \"debug\" mode only by default). // Default: true\n *         public_prefix?: scalar|Param|null, // The public path where the assets will be written to (and served from when \"server\" is true). // Default: \"/assets/\"\n *         missing_import_mode?: \"strict\"|\"warn\"|\"ignore\"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. \"import './non-existent.js'\". \"strict\" means an exception is thrown, \"warn\" means a warning is logged, \"ignore\" means the import is left as-is. // Default: \"warn\"\n *         extensions?: array<string, scalar|Param|null>,\n *         importmap_path?: scalar|Param|null, // The path of the importmap.php file. // Default: \"%kernel.project_dir%/importmap.php\"\n *         importmap_polyfill?: scalar|Param|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: \"es-module-shims\"\n *         importmap_script_attributes?: array<string, scalar|Param|null>,\n *         vendor_dir?: scalar|Param|null, // The directory to store JavaScript vendors. // Default: \"%kernel.project_dir%/assets/vendor\"\n *         precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip.\n *             enabled?: bool|Param, // Default: false\n *             formats?: list<scalar|Param|null>,\n *             extensions?: list<scalar|Param|null>,\n *         },\n *     },\n *     translator?: bool|array{ // Translator configuration\n *         enabled?: bool|Param, // Default: true\n *         fallbacks?: list<scalar|Param|null>,\n *         logging?: bool|Param, // Default: false\n *         formatter?: scalar|Param|null, // Default: \"translator.formatter.default\"\n *         cache_dir?: scalar|Param|null, // Default: \"%kernel.cache_dir%/translations\"\n *         default_path?: scalar|Param|null, // The default path used to load translations. // Default: \"%kernel.project_dir%/translations\"\n *         paths?: list<scalar|Param|null>,\n *         pseudo_localization?: bool|array{\n *             enabled?: bool|Param, // Default: false\n *             accents?: bool|Param, // Default: true\n *             expansion_factor?: float|Param, // Default: 1.0\n *             brackets?: bool|Param, // Default: true\n *             parse_html?: bool|Param, // Default: false\n *             localizable_html_attributes?: list<scalar|Param|null>,\n *         },\n *         providers?: array<string, array{ // Default: []\n *             dsn?: scalar|Param|null,\n *             domains?: list<scalar|Param|null>,\n *             locales?: list<scalar|Param|null>,\n *         }>,\n *         globals?: array<string, string|array{ // Default: []\n *             value?: mixed,\n *             message?: string|Param,\n *             parameters?: array<string, scalar|Param|null>,\n *             domain?: string|Param,\n *         }>,\n *     },\n *     validation?: bool|array{ // Validation configuration\n *         enabled?: bool|Param, // Default: true\n *         cache?: scalar|Param|null, // Deprecated: Setting the \"framework.validation.cache.cache\" configuration option is deprecated. It will be removed in version 8.0.\n *         enable_attributes?: bool|Param, // Default: true\n *         static_method?: list<scalar|Param|null>,\n *         translation_domain?: scalar|Param|null, // Default: \"validators\"\n *         email_validation_mode?: \"html5\"|\"html5-allow-no-tld\"|\"strict\"|\"loose\"|Param, // Default: \"html5\"\n *         mapping?: array{\n *             paths?: list<scalar|Param|null>,\n *         },\n *         not_compromised_password?: bool|array{\n *             enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true\n *             endpoint?: scalar|Param|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null\n *         },\n *         disable_translation?: bool|Param, // Default: false\n *         auto_mapping?: array<string, array{ // Default: []\n *             services?: list<scalar|Param|null>,\n *         }>,\n *     },\n *     annotations?: bool|array{\n *         enabled?: bool|Param, // Default: false\n *     },\n *     serializer?: bool|array{ // Serializer configuration\n *         enabled?: bool|Param, // Default: true\n *         enable_attributes?: bool|Param, // Default: true\n *         name_converter?: scalar|Param|null,\n *         circular_reference_handler?: scalar|Param|null,\n *         max_depth_handler?: scalar|Param|null,\n *         mapping?: array{\n *             paths?: list<scalar|Param|null>,\n *         },\n *         default_context?: array<string, mixed>,\n *         named_serializers?: array<string, array{ // Default: []\n *             name_converter?: scalar|Param|null,\n *             default_context?: array<string, mixed>,\n *             include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true\n *             include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true\n *         }>,\n *     },\n *     property_access?: bool|array{ // Property access configuration\n *         enabled?: bool|Param, // Default: true\n *         magic_call?: bool|Param, // Default: false\n *         magic_get?: bool|Param, // Default: true\n *         magic_set?: bool|Param, // Default: true\n *         throw_exception_on_invalid_index?: bool|Param, // Default: false\n *         throw_exception_on_invalid_property_path?: bool|Param, // Default: true\n *     },\n *     type_info?: bool|array{ // Type info configuration\n *         enabled?: bool|Param, // Default: true\n *         aliases?: array<string, scalar|Param|null>,\n *     },\n *     property_info?: bool|array{ // Property info configuration\n *         enabled?: bool|Param, // Default: true\n *         with_constructor_extractor?: bool|Param, // Registers the constructor extractor.\n *     },\n *     cache?: array{ // Cache configuration\n *         prefix_seed?: scalar|Param|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: \"_%kernel.project_dir%.%kernel.container_class%\"\n *         app?: scalar|Param|null, // App related cache pools configuration. // Default: \"cache.adapter.filesystem\"\n *         system?: scalar|Param|null, // System related cache pools configuration. // Default: \"cache.adapter.system\"\n *         directory?: scalar|Param|null, // Default: \"%kernel.share_dir%/pools/app\"\n *         default_psr6_provider?: scalar|Param|null,\n *         default_redis_provider?: scalar|Param|null, // Default: \"redis://localhost\"\n *         default_valkey_provider?: scalar|Param|null, // Default: \"valkey://localhost\"\n *         default_memcached_provider?: scalar|Param|null, // Default: \"memcached://localhost\"\n *         default_doctrine_dbal_provider?: scalar|Param|null, // Default: \"database_connection\"\n *         default_pdo_provider?: scalar|Param|null, // Default: null\n *         pools?: array<string, array{ // Default: []\n *             adapters?: list<scalar|Param|null>,\n *             tags?: scalar|Param|null, // Default: null\n *             public?: bool|Param, // Default: false\n *             default_lifetime?: scalar|Param|null, // Default lifetime of the pool.\n *             provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter.\n *             early_expiration_message_bus?: scalar|Param|null,\n *             clearer?: scalar|Param|null,\n *         }>,\n *     },\n *     php_errors?: array{ // PHP errors handling configuration\n *         log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true\n *         throw?: bool|Param, // Throw PHP errors as \\ErrorException instances. // Default: true\n *     },\n *     exceptions?: array<string, array{ // Default: []\n *         log_level?: scalar|Param|null, // The level of log message. Null to let Symfony decide. // Default: null\n *         status_code?: scalar|Param|null, // The status code of the response. Null or 0 to let Symfony decide. // Default: null\n *         log_channel?: scalar|Param|null, // The channel of log message. Null to let Symfony decide. // Default: null\n *     }>,\n *     web_link?: bool|array{ // Web links configuration\n *         enabled?: bool|Param, // Default: true\n *     },\n *     lock?: bool|string|array{ // Lock configuration\n *         enabled?: bool|Param, // Default: false\n *         resources?: array<string, string|list<scalar|Param|null>>,\n *     },\n *     semaphore?: bool|string|array{ // Semaphore configuration\n *         enabled?: bool|Param, // Default: false\n *         resources?: array<string, scalar|Param|null>,\n *     },\n *     messenger?: bool|array{ // Messenger configuration\n *         enabled?: bool|Param, // Default: false\n *         routing?: array<string, string|array{ // Default: []\n *             senders?: list<scalar|Param|null>,\n *         }>,\n *         serializer?: array{\n *             default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: \"messenger.transport.native_php_serializer\"\n *             symfony_serializer?: array{\n *                 format?: scalar|Param|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: \"json\"\n *                 context?: array<string, mixed>,\n *             },\n *         },\n *         transports?: array<string, string|array{ // Default: []\n *             dsn?: scalar|Param|null,\n *             serializer?: scalar|Param|null, // Service id of a custom serializer to use. // Default: null\n *             options?: array<string, mixed>,\n *             failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null\n *             retry_strategy?: string|array{\n *                 service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null\n *                 max_retries?: int|Param, // Default: 3\n *                 delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000\n *                 multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2\n *                 max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0\n *                 jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1\n *             },\n *             rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null\n *         }>,\n *         failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null\n *         stop_worker_on_signals?: list<scalar|Param|null>,\n *         default_bus?: scalar|Param|null, // Default: null\n *         buses?: array<string, array{ // Default: {\"messenger.bus.default\":{\"default_middleware\":{\"enabled\":true,\"allow_no_handlers\":false,\"allow_no_senders\":true},\"middleware\":[]}}\n *             default_middleware?: bool|string|array{\n *                 enabled?: bool|Param, // Default: true\n *                 allow_no_handlers?: bool|Param, // Default: false\n *                 allow_no_senders?: bool|Param, // Default: true\n *             },\n *             middleware?: list<string|array{ // Default: []\n *                 id?: scalar|Param|null,\n *                 arguments?: list<mixed>,\n *             }>,\n *         }>,\n *     },\n *     scheduler?: bool|array{ // Scheduler configuration\n *         enabled?: bool|Param, // Default: false\n *     },\n *     disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true\n *     http_client?: bool|array{ // HTTP Client configuration\n *         enabled?: bool|Param, // Default: true\n *         max_host_connections?: int|Param, // The maximum number of connections to a single host.\n *         default_options?: array{\n *             headers?: array<string, mixed>,\n *             vars?: array<string, mixed>,\n *             max_redirects?: int|Param, // The maximum number of redirects to follow.\n *             http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version.\n *             resolve?: array<string, scalar|Param|null>,\n *             proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection.\n *             no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached.\n *             timeout?: float|Param, // The idle timeout, defaults to the \"default_socket_timeout\" ini parameter.\n *             max_duration?: float|Param, // The maximum execution time for the request+response as a whole.\n *             bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to.\n *             verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context.\n *             verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name.\n *             cafile?: scalar|Param|null, // A certificate authority file.\n *             capath?: scalar|Param|null, // A directory that contains multiple certificate authority files.\n *             local_cert?: scalar|Param|null, // A PEM formatted certificate file.\n *             local_pk?: scalar|Param|null, // A private key file.\n *             passphrase?: scalar|Param|null, // The passphrase used to encrypt the \"local_pk\" file.\n *             ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. \"RC3-SHA:TLS13-AES-128-GCM-SHA256\"...)\n *             peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es).\n *                 sha1?: mixed,\n *                 pin-sha256?: mixed,\n *                 md5?: mixed,\n *             },\n *             crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants.\n *             extra?: array<string, mixed>,\n *             rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null\n *             caching?: bool|array{ // Caching configuration.\n *                 enabled?: bool|Param, // Default: false\n *                 cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: \"cache.http_client\"\n *                 shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true\n *                 max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null\n *             },\n *             retry_failed?: bool|array{\n *                 enabled?: bool|Param, // Default: false\n *                 retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null\n *                 http_codes?: array<string, array{ // Default: []\n *                     code?: int|Param,\n *                     methods?: list<string|Param>,\n *                 }>,\n *                 max_retries?: int|Param, // Default: 3\n *                 delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000\n *                 multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2\n *                 max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0\n *                 jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1\n *             },\n *         },\n *         mock_response_factory?: scalar|Param|null, // The id of the service that should generate mock responses. It should be either an invokable or an iterable.\n *         scoped_clients?: array<string, string|array{ // Default: []\n *             scope?: scalar|Param|null, // The regular expression that the request URL must match before adding the other options. When none is provided, the base URI is used instead.\n *             base_uri?: scalar|Param|null, // The URI to resolve relative URLs, following rules in RFC 3985, section 2.\n *             auth_basic?: scalar|Param|null, // An HTTP Basic authentication \"username:password\".\n *             auth_bearer?: scalar|Param|null, // A token enabling HTTP Bearer authorization.\n *             auth_ntlm?: scalar|Param|null, // A \"username:password\" pair to use Microsoft NTLM authentication (requires the cURL extension).\n *             query?: array<string, scalar|Param|null>,\n *             headers?: array<string, mixed>,\n *             max_redirects?: int|Param, // The maximum number of redirects to follow.\n *             http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version.\n *             resolve?: array<string, scalar|Param|null>,\n *             proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection.\n *             no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached.\n *             timeout?: float|Param, // The idle timeout, defaults to the \"default_socket_timeout\" ini parameter.\n *             max_duration?: float|Param, // The maximum execution time for the request+response as a whole.\n *             bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to.\n *             verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context.\n *             verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name.\n *             cafile?: scalar|Param|null, // A certificate authority file.\n *             capath?: scalar|Param|null, // A directory that contains multiple certificate authority files.\n *             local_cert?: scalar|Param|null, // A PEM formatted certificate file.\n *             local_pk?: scalar|Param|null, // A private key file.\n *             passphrase?: scalar|Param|null, // The passphrase used to encrypt the \"local_pk\" file.\n *             ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. \"RC3-SHA:TLS13-AES-128-GCM-SHA256\"...).\n *             peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es).\n *                 sha1?: mixed,\n *                 pin-sha256?: mixed,\n *                 md5?: mixed,\n *             },\n *             crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants.\n *             extra?: array<string, mixed>,\n *             rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null\n *             caching?: bool|array{ // Caching configuration.\n *                 enabled?: bool|Param, // Default: false\n *                 cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: \"cache.http_client\"\n *                 shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true\n *                 max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null\n *             },\n *             retry_failed?: bool|array{\n *                 enabled?: bool|Param, // Default: false\n *                 retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null\n *                 http_codes?: array<string, array{ // Default: []\n *                     code?: int|Param,\n *                     methods?: list<string|Param>,\n *                 }>,\n *                 max_retries?: int|Param, // Default: 3\n *                 delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000\n *                 multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2\n *                 max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0\n *                 jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1\n *             },\n *         }>,\n *     },\n *     mailer?: bool|array{ // Mailer configuration\n *         enabled?: bool|Param, // Default: true\n *         message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null\n *         dsn?: scalar|Param|null, // Default: null\n *         transports?: array<string, scalar|Param|null>,\n *         envelope?: array{ // Mailer Envelope configuration\n *             sender?: scalar|Param|null,\n *             recipients?: list<scalar|Param|null>,\n *             allowed_recipients?: list<scalar|Param|null>,\n *         },\n *         headers?: array<string, string|array{ // Default: []\n *             value?: mixed,\n *         }>,\n *         dkim_signer?: bool|array{ // DKIM signer configuration\n *             enabled?: bool|Param, // Default: false\n *             key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: \"\"\n *             domain?: scalar|Param|null, // Default: \"\"\n *             select?: scalar|Param|null, // Default: \"\"\n *             passphrase?: scalar|Param|null, // The private key passphrase // Default: \"\"\n *             options?: array<string, mixed>,\n *         },\n *         smime_signer?: bool|array{ // S/MIME signer configuration\n *             enabled?: bool|Param, // Default: false\n *             key?: scalar|Param|null, // Path to key (in PEM format) // Default: \"\"\n *             certificate?: scalar|Param|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: \"\"\n *             passphrase?: scalar|Param|null, // The private key passphrase // Default: null\n *             extra_certificates?: scalar|Param|null, // Default: null\n *             sign_options?: int|Param, // Default: null\n *         },\n *         smime_encrypter?: bool|array{ // S/MIME encrypter configuration\n *             enabled?: bool|Param, // Default: false\n *             repository?: scalar|Param|null, // S/MIME certificate repository service. This service shall implement the `Symfony\\Component\\Mailer\\EventListener\\SmimeCertificateRepositoryInterface`. // Default: \"\"\n *             cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null\n *         },\n *     },\n *     secrets?: bool|array{\n *         enabled?: bool|Param, // Default: true\n *         vault_directory?: scalar|Param|null, // Default: \"%kernel.project_dir%/config/secrets/%kernel.runtime_environment%\"\n *         local_dotenv_file?: scalar|Param|null, // Default: \"%kernel.project_dir%/.env.%kernel.environment%.local\"\n *         decryption_env_var?: scalar|Param|null, // Default: \"base64:default::SYMFONY_DECRYPTION_SECRET\"\n *     },\n *     notifier?: bool|array{ // Notifier configuration\n *         enabled?: bool|Param, // Default: false\n *         message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null\n *         chatter_transports?: array<string, scalar|Param|null>,\n *         texter_transports?: array<string, scalar|Param|null>,\n *         notification_on_failed_messages?: bool|Param, // Default: false\n *         channel_policy?: array<string, string|list<scalar|Param|null>>,\n *         admin_recipients?: list<array{ // Default: []\n *             email?: scalar|Param|null,\n *             phone?: scalar|Param|null, // Default: \"\"\n *         }>,\n *     },\n *     rate_limiter?: bool|array{ // Rate limiter configuration\n *         enabled?: bool|Param, // Default: false\n *         limiters?: array<string, array{ // Default: []\n *             lock_factory?: scalar|Param|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: \"auto\"\n *             cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: \"cache.rate_limiter\"\n *             storage_service?: scalar|Param|null, // The service ID of a custom storage implementation, this precedes any configured \"cache_pool\". // Default: null\n *             policy?: \"fixed_window\"|\"token_bucket\"|\"sliding_window\"|\"compound\"|\"no_limit\"|Param, // The algorithm to be used by this limiter.\n *             limiters?: list<scalar|Param|null>,\n *             limit?: int|Param, // The maximum allowed hits in a fixed interval or burst.\n *             interval?: scalar|Param|null, // Configures the fixed interval if \"policy\" is set to \"fixed_window\" or \"sliding_window\". The value must be a number followed by \"second\", \"minute\", \"hour\", \"day\", \"week\" or \"month\" (or their plural equivalent).\n *             rate?: array{ // Configures the fill rate if \"policy\" is set to \"token_bucket\".\n *                 interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by \"second\", \"minute\", \"hour\", \"day\", \"week\" or \"month\" (or their plural equivalent).\n *                 amount?: int|Param, // Amount of tokens to add each interval. // Default: 1\n *             },\n *         }>,\n *     },\n *     uid?: bool|array{ // Uid configuration\n *         enabled?: bool|Param, // Default: false\n *         default_uuid_version?: 7|6|4|1|Param, // Default: 7\n *         name_based_uuid_version?: 5|3|Param, // Default: 5\n *         name_based_uuid_namespace?: scalar|Param|null,\n *         time_based_uuid_version?: 7|6|1|Param, // Default: 7\n *         time_based_uuid_node?: scalar|Param|null,\n *     },\n *     html_sanitizer?: bool|array{ // HtmlSanitizer configuration\n *         enabled?: bool|Param, // Default: false\n *         sanitizers?: array<string, array{ // Default: []\n *             allow_safe_elements?: bool|Param, // Allows \"safe\" elements and attributes. // Default: false\n *             allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false\n *             allow_elements?: array<string, mixed>,\n *             block_elements?: list<string|Param>,\n *             drop_elements?: list<string|Param>,\n *             allow_attributes?: array<string, mixed>,\n *             drop_attributes?: array<string, mixed>,\n *             force_attributes?: array<string, array<string, string|Param>>,\n *             force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false\n *             allowed_link_schemes?: list<string|Param>,\n *             allowed_link_hosts?: list<string|Param>|null,\n *             allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false\n *             allowed_media_schemes?: list<string|Param>,\n *             allowed_media_hosts?: list<string|Param>|null,\n *             allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false\n *             with_attribute_sanitizers?: list<string|Param>,\n *             without_attribute_sanitizers?: list<string|Param>,\n *             max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0\n *         }>,\n *     },\n *     webhook?: bool|array{ // Webhook configuration\n *         enabled?: bool|Param, // Default: false\n *         message_bus?: scalar|Param|null, // The message bus to use. // Default: \"messenger.default_bus\"\n *         routing?: array<string, array{ // Default: []\n *             service?: scalar|Param|null,\n *             secret?: scalar|Param|null, // Default: \"\"\n *         }>,\n *     },\n *     remote-event?: bool|array{ // RemoteEvent configuration\n *         enabled?: bool|Param, // Default: false\n *     },\n *     json_streamer?: bool|array{ // JSON streamer configuration\n *         enabled?: bool|Param, // Default: false\n *     },\n * }\n * @psalm-type DoctrineConfig = array{\n *     dbal?: array{\n *         default_connection?: scalar|Param|null,\n *         types?: array<string, string|array{ // Default: []\n *             class?: scalar|Param|null,\n *             commented?: bool|Param, // Deprecated: The doctrine-bundle type commenting features were removed; the corresponding config parameter was deprecated in 2.0 and will be dropped in 3.0.\n *         }>,\n *         driver_schemes?: array<string, scalar|Param|null>,\n *         connections?: array<string, array{ // Default: []\n *             url?: scalar|Param|null, // A URL with connection information; any parameter value parsed from this string will override explicitly set parameters\n *             dbname?: scalar|Param|null,\n *             host?: scalar|Param|null, // Defaults to \"localhost\" at runtime.\n *             port?: scalar|Param|null, // Defaults to null at runtime.\n *             user?: scalar|Param|null, // Defaults to \"root\" at runtime.\n *             password?: scalar|Param|null, // Defaults to null at runtime.\n *             override_url?: bool|Param, // Deprecated: The \"doctrine.dbal.override_url\" configuration key is deprecated.\n *             dbname_suffix?: scalar|Param|null, // Adds the given suffix to the configured database name, this option has no effects for the SQLite platform\n *             application_name?: scalar|Param|null,\n *             charset?: scalar|Param|null,\n *             path?: scalar|Param|null,\n *             memory?: bool|Param,\n *             unix_socket?: scalar|Param|null, // The unix socket to use for MySQL\n *             persistent?: bool|Param, // True to use as persistent connection for the ibm_db2 driver\n *             protocol?: scalar|Param|null, // The protocol to use for the ibm_db2 driver (default to TCPIP if omitted)\n *             service?: bool|Param, // True to use SERVICE_NAME as connection parameter instead of SID for Oracle\n *             servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.\n *             sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver\n *             server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.\n *             default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.\n *             sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.\n *             sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.\n *             sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.\n *             sslkey?: scalar|Param|null, // The path to the SSL client key file for PostgreSQL.\n *             sslcrl?: scalar|Param|null, // The file name of the SSL certificate revocation list for PostgreSQL.\n *             pooled?: bool|Param, // True to use a pooled server with the oci8/pdo_oracle driver\n *             MultipleActiveResultSets?: bool|Param, // Configuring MultipleActiveResultSets for the pdo_sqlsrv driver\n *             use_savepoints?: bool|Param, // Use savepoints for nested transactions\n *             instancename?: scalar|Param|null, // Optional parameter, complete whether to add the INSTANCE_NAME parameter in the connection. It is generally used to connect to an Oracle RAC server to select the name of a particular instance.\n *             connectstring?: scalar|Param|null, // Complete Easy Connect connection descriptor, see https://docs.oracle.com/database/121/NETAG/naming.htm.When using this option, you will still need to provide the user and password parameters, but the other parameters will no longer be used. Note that when using this parameter, the getHost and getPort methods from Doctrine\\DBAL\\Connection will no longer function as expected.\n *             driver?: scalar|Param|null, // Default: \"pdo_mysql\"\n *             platform_service?: scalar|Param|null, // Deprecated: The \"platform_service\" configuration key is deprecated since doctrine-bundle 2.9. DBAL 4 will not support setting a custom platform via connection params anymore.\n *             auto_commit?: bool|Param,\n *             schema_filter?: scalar|Param|null,\n *             logging?: bool|Param, // Default: true\n *             profiling?: bool|Param, // Default: true\n *             profiling_collect_backtrace?: bool|Param, // Enables collecting backtraces when profiling is enabled // Default: false\n *             profiling_collect_schema_errors?: bool|Param, // Enables collecting schema errors when profiling is enabled // Default: true\n *             disable_type_comments?: bool|Param,\n *             server_version?: scalar|Param|null,\n *             idle_connection_ttl?: int|Param, // Default: 600\n *             driver_class?: scalar|Param|null,\n *             wrapper_class?: scalar|Param|null,\n *             keep_slave?: bool|Param, // Deprecated: The \"keep_slave\" configuration key is deprecated since doctrine-bundle 2.2. Use the \"keep_replica\" configuration key instead.\n *             keep_replica?: bool|Param,\n *             options?: array<string, mixed>,\n *             mapping_types?: array<string, scalar|Param|null>,\n *             default_table_options?: array<string, scalar|Param|null>,\n *             schema_manager_factory?: scalar|Param|null, // Default: \"doctrine.dbal.legacy_schema_manager_factory\"\n *             result_cache?: scalar|Param|null,\n *             slaves?: array<string, array{ // Default: []\n *                 url?: scalar|Param|null, // A URL with connection information; any parameter value parsed from this string will override explicitly set parameters\n *                 dbname?: scalar|Param|null,\n *                 host?: scalar|Param|null, // Defaults to \"localhost\" at runtime.\n *                 port?: scalar|Param|null, // Defaults to null at runtime.\n *                 user?: scalar|Param|null, // Defaults to \"root\" at runtime.\n *                 password?: scalar|Param|null, // Defaults to null at runtime.\n *                 override_url?: bool|Param, // Deprecated: The \"doctrine.dbal.override_url\" configuration key is deprecated.\n *                 dbname_suffix?: scalar|Param|null, // Adds the given suffix to the configured database name, this option has no effects for the SQLite platform\n *                 application_name?: scalar|Param|null,\n *                 charset?: scalar|Param|null,\n *                 path?: scalar|Param|null,\n *                 memory?: bool|Param,\n *                 unix_socket?: scalar|Param|null, // The unix socket to use for MySQL\n *                 persistent?: bool|Param, // True to use as persistent connection for the ibm_db2 driver\n *                 protocol?: scalar|Param|null, // The protocol to use for the ibm_db2 driver (default to TCPIP if omitted)\n *                 service?: bool|Param, // True to use SERVICE_NAME as connection parameter instead of SID for Oracle\n *                 servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.\n *                 sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver\n *                 server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.\n *                 default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.\n *                 sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.\n *                 sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.\n *                 sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.\n *                 sslkey?: scalar|Param|null, // The path to the SSL client key file for PostgreSQL.\n *                 sslcrl?: scalar|Param|null, // The file name of the SSL certificate revocation list for PostgreSQL.\n *                 pooled?: bool|Param, // True to use a pooled server with the oci8/pdo_oracle driver\n *                 MultipleActiveResultSets?: bool|Param, // Configuring MultipleActiveResultSets for the pdo_sqlsrv driver\n *                 use_savepoints?: bool|Param, // Use savepoints for nested transactions\n *                 instancename?: scalar|Param|null, // Optional parameter, complete whether to add the INSTANCE_NAME parameter in the connection. It is generally used to connect to an Oracle RAC server to select the name of a particular instance.\n *                 connectstring?: scalar|Param|null, // Complete Easy Connect connection descriptor, see https://docs.oracle.com/database/121/NETAG/naming.htm.When using this option, you will still need to provide the user and password parameters, but the other parameters will no longer be used. Note that when using this parameter, the getHost and getPort methods from Doctrine\\DBAL\\Connection will no longer function as expected.\n *             }>,\n *             replicas?: array<string, array{ // Default: []\n *                 url?: scalar|Param|null, // A URL with connection information; any parameter value parsed from this string will override explicitly set parameters\n *                 dbname?: scalar|Param|null,\n *                 host?: scalar|Param|null, // Defaults to \"localhost\" at runtime.\n *                 port?: scalar|Param|null, // Defaults to null at runtime.\n *                 user?: scalar|Param|null, // Defaults to \"root\" at runtime.\n *                 password?: scalar|Param|null, // Defaults to null at runtime.\n *                 override_url?: bool|Param, // Deprecated: The \"doctrine.dbal.override_url\" configuration key is deprecated.\n *                 dbname_suffix?: scalar|Param|null, // Adds the given suffix to the configured database name, this option has no effects for the SQLite platform\n *                 application_name?: scalar|Param|null,\n *                 charset?: scalar|Param|null,\n *                 path?: scalar|Param|null,\n *                 memory?: bool|Param,\n *                 unix_socket?: scalar|Param|null, // The unix socket to use for MySQL\n *                 persistent?: bool|Param, // True to use as persistent connection for the ibm_db2 driver\n *                 protocol?: scalar|Param|null, // The protocol to use for the ibm_db2 driver (default to TCPIP if omitted)\n *                 service?: bool|Param, // True to use SERVICE_NAME as connection parameter instead of SID for Oracle\n *                 servicename?: scalar|Param|null, // Overrules dbname parameter if given and used as SERVICE_NAME or SID connection parameter for Oracle depending on the service parameter.\n *                 sessionMode?: scalar|Param|null, // The session mode to use for the oci8 driver\n *                 server?: scalar|Param|null, // The name of a running database server to connect to for SQL Anywhere.\n *                 default_dbname?: scalar|Param|null, // Override the default database (postgres) to connect to for PostgreSQL connexion.\n *                 sslmode?: scalar|Param|null, // Determines whether or with what priority a SSL TCP/IP connection will be negotiated with the server for PostgreSQL.\n *                 sslrootcert?: scalar|Param|null, // The name of a file containing SSL certificate authority (CA) certificate(s). If the file exists, the server's certificate will be verified to be signed by one of these authorities.\n *                 sslcert?: scalar|Param|null, // The path to the SSL client certificate file for PostgreSQL.\n *                 sslkey?: scalar|Param|null, // The path to the SSL client key file for PostgreSQL.\n *                 sslcrl?: scalar|Param|null, // The file name of the SSL certificate revocation list for PostgreSQL.\n *                 pooled?: bool|Param, // True to use a pooled server with the oci8/pdo_oracle driver\n *                 MultipleActiveResultSets?: bool|Param, // Configuring MultipleActiveResultSets for the pdo_sqlsrv driver\n *                 use_savepoints?: bool|Param, // Use savepoints for nested transactions\n *                 instancename?: scalar|Param|null, // Optional parameter, complete whether to add the INSTANCE_NAME parameter in the connection. It is generally used to connect to an Oracle RAC server to select the name of a particular instance.\n *                 connectstring?: scalar|Param|null, // Complete Easy Connect connection descriptor, see https://docs.oracle.com/database/121/NETAG/naming.htm.When using this option, you will still need to provide the user and password parameters, but the other parameters will no longer be used. Note that when using this parameter, the getHost and getPort methods from Doctrine\\DBAL\\Connection will no longer function as expected.\n *             }>,\n *         }>,\n *     },\n *     orm?: array{\n *         default_entity_manager?: scalar|Param|null,\n *         auto_generate_proxy_classes?: scalar|Param|null, // Auto generate mode possible values are: \"NEVER\", \"ALWAYS\", \"FILE_NOT_EXISTS\", \"EVAL\", \"FILE_NOT_EXISTS_OR_CHANGED\", this option is ignored when the \"enable_native_lazy_objects\" option is true // Default: false\n *         enable_lazy_ghost_objects?: bool|Param, // Enables the new implementation of proxies based on lazy ghosts instead of using the legacy implementation // Default: false\n *         enable_native_lazy_objects?: bool|Param, // Enables the new native implementation of PHP lazy objects instead of generated proxies // Default: false\n *         proxy_dir?: scalar|Param|null, // Configures the path where generated proxy classes are saved when using non-native lazy objects, this option is ignored when the \"enable_native_lazy_objects\" option is true // Default: \"%kernel.build_dir%/doctrine/orm/Proxies\"\n *         proxy_namespace?: scalar|Param|null, // Defines the root namespace for generated proxy classes when using non-native lazy objects, this option is ignored when the \"enable_native_lazy_objects\" option is true // Default: \"Proxies\"\n *         controller_resolver?: bool|array{\n *             enabled?: bool|Param, // Default: true\n *             auto_mapping?: bool|Param|null, // Set to false to disable using route placeholders as lookup criteria when the primary key doesn't match the argument name // Default: null\n *             evict_cache?: bool|Param, // Set to true to fetch the entity from the database instead of using the cache, if any // Default: false\n *         },\n *         entity_managers?: array<string, array{ // Default: []\n *             query_cache_driver?: string|array{\n *                 type?: scalar|Param|null, // Default: null\n *                 id?: scalar|Param|null,\n *                 pool?: scalar|Param|null,\n *             },\n *             metadata_cache_driver?: string|array{\n *                 type?: scalar|Param|null, // Default: null\n *                 id?: scalar|Param|null,\n *                 pool?: scalar|Param|null,\n *             },\n *             result_cache_driver?: string|array{\n *                 type?: scalar|Param|null, // Default: null\n *                 id?: scalar|Param|null,\n *                 pool?: scalar|Param|null,\n *             },\n *             entity_listeners?: array{\n *                 entities?: array<string, array{ // Default: []\n *                     listeners?: array<string, array{ // Default: []\n *                         events?: list<array{ // Default: []\n *                             type?: scalar|Param|null,\n *                             method?: scalar|Param|null, // Default: null\n *                         }>,\n *                     }>,\n *                 }>,\n *             },\n *             connection?: scalar|Param|null,\n *             class_metadata_factory_name?: scalar|Param|null, // Default: \"Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadataFactory\"\n *             default_repository_class?: scalar|Param|null, // Default: \"Doctrine\\\\ORM\\\\EntityRepository\"\n *             auto_mapping?: scalar|Param|null, // Default: false\n *             naming_strategy?: scalar|Param|null, // Default: \"doctrine.orm.naming_strategy.default\"\n *             quote_strategy?: scalar|Param|null, // Default: \"doctrine.orm.quote_strategy.default\"\n *             typed_field_mapper?: scalar|Param|null, // Default: \"doctrine.orm.typed_field_mapper.default\"\n *             entity_listener_resolver?: scalar|Param|null, // Default: null\n *             fetch_mode_subselect_batch_size?: scalar|Param|null,\n *             repository_factory?: scalar|Param|null, // Default: \"doctrine.orm.container_repository_factory\"\n *             schema_ignore_classes?: list<scalar|Param|null>,\n *             report_fields_where_declared?: bool|Param, // Set to \"true\" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.16 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/10455. // Default: false\n *             validate_xml_mapping?: bool|Param, // Set to \"true\" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.14. See https://github.com/doctrine/orm/pull/6728. // Default: false\n *             second_level_cache?: array{\n *                 region_cache_driver?: string|array{\n *                     type?: scalar|Param|null, // Default: null\n *                     id?: scalar|Param|null,\n *                     pool?: scalar|Param|null,\n *                 },\n *                 region_lock_lifetime?: scalar|Param|null, // Default: 60\n *                 log_enabled?: bool|Param, // Default: true\n *                 region_lifetime?: scalar|Param|null, // Default: 3600\n *                 enabled?: bool|Param, // Default: true\n *                 factory?: scalar|Param|null,\n *                 regions?: array<string, array{ // Default: []\n *                     cache_driver?: string|array{\n *                         type?: scalar|Param|null, // Default: null\n *                         id?: scalar|Param|null,\n *                         pool?: scalar|Param|null,\n *                     },\n *                     lock_path?: scalar|Param|null, // Default: \"%kernel.cache_dir%/doctrine/orm/slc/filelock\"\n *                     lock_lifetime?: scalar|Param|null, // Default: 60\n *                     type?: scalar|Param|null, // Default: \"default\"\n *                     lifetime?: scalar|Param|null, // Default: 0\n *                     service?: scalar|Param|null,\n *                     name?: scalar|Param|null,\n *                 }>,\n *                 loggers?: array<string, array{ // Default: []\n *                     name?: scalar|Param|null,\n *                     service?: scalar|Param|null,\n *                 }>,\n *             },\n *             hydrators?: array<string, scalar|Param|null>,\n *             mappings?: array<string, bool|string|array{ // Default: []\n *                 mapping?: scalar|Param|null, // Default: true\n *                 type?: scalar|Param|null,\n *                 dir?: scalar|Param|null,\n *                 alias?: scalar|Param|null,\n *                 prefix?: scalar|Param|null,\n *                 is_bundle?: bool|Param,\n *             }>,\n *             dql?: array{\n *                 string_functions?: array<string, scalar|Param|null>,\n *                 numeric_functions?: array<string, scalar|Param|null>,\n *                 datetime_functions?: array<string, scalar|Param|null>,\n *             },\n *             filters?: array<string, string|array{ // Default: []\n *                 class?: scalar|Param|null,\n *                 enabled?: bool|Param, // Default: false\n *                 parameters?: array<string, mixed>,\n *             }>,\n *             identity_generation_preferences?: array<string, scalar|Param|null>,\n *         }>,\n *         resolve_target_entities?: array<string, scalar|Param|null>,\n *     },\n * }\n * @psalm-type DoctrineMigrationsConfig = array{\n *     enable_service_migrations?: bool|Param, // Whether to enable fetching migrations from the service container. // Default: false\n *     migrations_paths?: array<string, scalar|Param|null>,\n *     services?: array<string, scalar|Param|null>,\n *     factories?: array<string, scalar|Param|null>,\n *     storage?: array{ // Storage to use for migration status metadata.\n *         table_storage?: array{ // The default metadata storage, implemented as a table in the database.\n *             table_name?: scalar|Param|null, // Default: null\n *             version_column_name?: scalar|Param|null, // Default: null\n *             version_column_length?: scalar|Param|null, // Default: null\n *             executed_at_column_name?: scalar|Param|null, // Default: null\n *             execution_time_column_name?: scalar|Param|null, // Default: null\n *         },\n *     },\n *     migrations?: list<scalar|Param|null>,\n *     connection?: scalar|Param|null, // Connection name to use for the migrations database. // Default: null\n *     em?: scalar|Param|null, // Entity manager name to use for the migrations database (available when doctrine/orm is installed). // Default: null\n *     all_or_nothing?: scalar|Param|null, // Run all migrations in a transaction. // Default: false\n *     check_database_platform?: scalar|Param|null, // Adds an extra check in the generated migrations to allow execution only on the same platform as they were initially generated on. // Default: true\n *     custom_template?: scalar|Param|null, // Custom template path for generated migration classes. // Default: null\n *     organize_migrations?: scalar|Param|null, // Organize migrations mode. Possible values are: \"BY_YEAR\", \"BY_YEAR_AND_MONTH\", false // Default: false\n *     enable_profiler?: bool|Param, // Whether or not to enable the profiler collector to calculate and visualize migration status. This adds some queries overhead. // Default: false\n *     transactional?: bool|Param, // Whether or not to wrap migrations in a single transaction. // Default: true\n * }\n * @psalm-type SecurityConfig = array{\n *     access_denied_url?: scalar|Param|null, // Default: null\n *     session_fixation_strategy?: \"none\"|\"migrate\"|\"invalidate\"|Param, // Default: \"migrate\"\n *     hide_user_not_found?: bool|Param, // Deprecated: The \"hide_user_not_found\" option is deprecated and will be removed in 8.0. Use the \"expose_security_errors\" option instead.\n *     expose_security_errors?: \\Symfony\\Component\\Security\\Http\\Authentication\\ExposeSecurityLevel::None|\\Symfony\\Component\\Security\\Http\\Authentication\\ExposeSecurityLevel::AccountStatus|\\Symfony\\Component\\Security\\Http\\Authentication\\ExposeSecurityLevel::All|Param, // Default: \"none\"\n *     erase_credentials?: bool|Param, // Default: true\n *     access_decision_manager?: array{\n *         strategy?: \"affirmative\"|\"consensus\"|\"unanimous\"|\"priority\"|Param,\n *         service?: scalar|Param|null,\n *         strategy_service?: scalar|Param|null,\n *         allow_if_all_abstain?: bool|Param, // Default: false\n *         allow_if_equal_granted_denied?: bool|Param, // Default: true\n *     },\n *     password_hashers?: array<string, string|array{ // Default: []\n *         algorithm?: scalar|Param|null,\n *         migrate_from?: list<scalar|Param|null>,\n *         hash_algorithm?: scalar|Param|null, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: \"sha512\"\n *         key_length?: scalar|Param|null, // Default: 40\n *         ignore_case?: bool|Param, // Default: false\n *         encode_as_base64?: bool|Param, // Default: true\n *         iterations?: scalar|Param|null, // Default: 5000\n *         cost?: int|Param, // Default: null\n *         memory_cost?: scalar|Param|null, // Default: null\n *         time_cost?: scalar|Param|null, // Default: null\n *         id?: scalar|Param|null,\n *     }>,\n *     providers?: array<string, array{ // Default: []\n *         id?: scalar|Param|null,\n *         chain?: array{\n *             providers?: list<scalar|Param|null>,\n *         },\n *         entity?: array{\n *             class?: scalar|Param|null, // The full entity class name of your user class.\n *             property?: scalar|Param|null, // Default: null\n *             manager_name?: scalar|Param|null, // Default: null\n *         },\n *         memory?: array{\n *             users?: array<string, array{ // Default: []\n *                 password?: scalar|Param|null, // Default: null\n *                 roles?: list<scalar|Param|null>,\n *             }>,\n *         },\n *         ldap?: array{\n *             service?: scalar|Param|null,\n *             base_dn?: scalar|Param|null,\n *             search_dn?: scalar|Param|null, // Default: null\n *             search_password?: scalar|Param|null, // Default: null\n *             extra_fields?: list<scalar|Param|null>,\n *             default_roles?: list<scalar|Param|null>,\n *             role_fetcher?: scalar|Param|null, // Default: null\n *             uid_key?: scalar|Param|null, // Default: \"sAMAccountName\"\n *             filter?: scalar|Param|null, // Default: \"({uid_key}={user_identifier})\"\n *             password_attribute?: scalar|Param|null, // Default: null\n *         },\n *     }>,\n *     firewalls?: array<string, array{ // Default: []\n *         pattern?: scalar|Param|null,\n *         host?: scalar|Param|null,\n *         methods?: list<scalar|Param|null>,\n *         security?: bool|Param, // Default: true\n *         user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: \"security.user_checker\"\n *         request_matcher?: scalar|Param|null,\n *         access_denied_url?: scalar|Param|null,\n *         access_denied_handler?: scalar|Param|null,\n *         entry_point?: scalar|Param|null, // An enabled authenticator name or a service id that implements \"Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface\".\n *         provider?: scalar|Param|null,\n *         stateless?: bool|Param, // Default: false\n *         lazy?: bool|Param, // Default: false\n *         context?: scalar|Param|null,\n *         logout?: array{\n *             enable_csrf?: bool|Param|null, // Default: null\n *             csrf_token_id?: scalar|Param|null, // Default: \"logout\"\n *             csrf_parameter?: scalar|Param|null, // Default: \"_csrf_token\"\n *             csrf_token_manager?: scalar|Param|null,\n *             path?: scalar|Param|null, // Default: \"/logout\"\n *             target?: scalar|Param|null, // Default: \"/\"\n *             invalidate_session?: bool|Param, // Default: true\n *             clear_site_data?: list<\"*\"|\"cache\"|\"cookies\"|\"storage\"|\"executionContexts\"|Param>,\n *             delete_cookies?: array<string, array{ // Default: []\n *                 path?: scalar|Param|null, // Default: null\n *                 domain?: scalar|Param|null, // Default: null\n *                 secure?: scalar|Param|null, // Default: false\n *                 samesite?: scalar|Param|null, // Default: null\n *                 partitioned?: scalar|Param|null, // Default: false\n *             }>,\n *         },\n *         switch_user?: array{\n *             provider?: scalar|Param|null,\n *             parameter?: scalar|Param|null, // Default: \"_switch_user\"\n *             role?: scalar|Param|null, // Default: \"ROLE_ALLOWED_TO_SWITCH\"\n *             target_route?: scalar|Param|null, // Default: null\n *         },\n *         required_badges?: list<scalar|Param|null>,\n *         custom_authenticators?: list<scalar|Param|null>,\n *         login_throttling?: array{\n *             limiter?: scalar|Param|null, // A service id implementing \"Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface\".\n *             max_attempts?: int|Param, // Default: 5\n *             interval?: scalar|Param|null, // Default: \"1 minute\"\n *             lock_factory?: scalar|Param|null, // The service ID of the lock factory used by the login rate limiter (or null to disable locking). // Default: null\n *             cache_pool?: string|Param, // The cache pool to use for storing the limiter state // Default: \"cache.rate_limiter\"\n *             storage_service?: string|Param, // The service ID of a custom storage implementation, this precedes any configured \"cache_pool\" // Default: null\n *         },\n *         x509?: array{\n *             provider?: scalar|Param|null,\n *             user?: scalar|Param|null, // Default: \"SSL_CLIENT_S_DN_Email\"\n *             credentials?: scalar|Param|null, // Default: \"SSL_CLIENT_S_DN\"\n *             user_identifier?: scalar|Param|null, // Default: \"emailAddress\"\n *         },\n *         remote_user?: array{\n *             provider?: scalar|Param|null,\n *             user?: scalar|Param|null, // Default: \"REMOTE_USER\"\n *         },\n *         login_link?: array{\n *             check_route?: scalar|Param|null, // Route that will validate the login link - e.g. \"app_login_link_verify\".\n *             check_post_only?: scalar|Param|null, // If true, only HTTP POST requests to \"check_route\" will be handled by the authenticator. // Default: false\n *             signature_properties?: list<scalar|Param|null>,\n *             lifetime?: int|Param, // The lifetime of the login link in seconds. // Default: 600\n *             max_uses?: int|Param, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null\n *             used_link_cache?: scalar|Param|null, // Cache service id used to expired links of max_uses is set.\n *             success_handler?: scalar|Param|null, // A service id that implements Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationSuccessHandlerInterface.\n *             failure_handler?: scalar|Param|null, // A service id that implements Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationFailureHandlerInterface.\n *             provider?: scalar|Param|null, // The user provider to load users from.\n *             secret?: scalar|Param|null, // Default: \"%kernel.secret%\"\n *             always_use_default_target_path?: bool|Param, // Default: false\n *             default_target_path?: scalar|Param|null, // Default: \"/\"\n *             login_path?: scalar|Param|null, // Default: \"/login\"\n *             target_path_parameter?: scalar|Param|null, // Default: \"_target_path\"\n *             use_referer?: bool|Param, // Default: false\n *             failure_path?: scalar|Param|null, // Default: null\n *             failure_forward?: bool|Param, // Default: false\n *             failure_path_parameter?: scalar|Param|null, // Default: \"_failure_path\"\n *         },\n *         form_login?: array{\n *             provider?: scalar|Param|null,\n *             remember_me?: bool|Param, // Default: true\n *             success_handler?: scalar|Param|null,\n *             failure_handler?: scalar|Param|null,\n *             check_path?: scalar|Param|null, // Default: \"/login_check\"\n *             use_forward?: bool|Param, // Default: false\n *             login_path?: scalar|Param|null, // Default: \"/login\"\n *             username_parameter?: scalar|Param|null, // Default: \"_username\"\n *             password_parameter?: scalar|Param|null, // Default: \"_password\"\n *             csrf_parameter?: scalar|Param|null, // Default: \"_csrf_token\"\n *             csrf_token_id?: scalar|Param|null, // Default: \"authenticate\"\n *             enable_csrf?: bool|Param, // Default: false\n *             post_only?: bool|Param, // Default: true\n *             form_only?: bool|Param, // Default: false\n *             always_use_default_target_path?: bool|Param, // Default: false\n *             default_target_path?: scalar|Param|null, // Default: \"/\"\n *             target_path_parameter?: scalar|Param|null, // Default: \"_target_path\"\n *             use_referer?: bool|Param, // Default: false\n *             failure_path?: scalar|Param|null, // Default: null\n *             failure_forward?: bool|Param, // Default: false\n *             failure_path_parameter?: scalar|Param|null, // Default: \"_failure_path\"\n *         },\n *         form_login_ldap?: array{\n *             provider?: scalar|Param|null,\n *             remember_me?: bool|Param, // Default: true\n *             success_handler?: scalar|Param|null,\n *             failure_handler?: scalar|Param|null,\n *             check_path?: scalar|Param|null, // Default: \"/login_check\"\n *             use_forward?: bool|Param, // Default: false\n *             login_path?: scalar|Param|null, // Default: \"/login\"\n *             username_parameter?: scalar|Param|null, // Default: \"_username\"\n *             password_parameter?: scalar|Param|null, // Default: \"_password\"\n *             csrf_parameter?: scalar|Param|null, // Default: \"_csrf_token\"\n *             csrf_token_id?: scalar|Param|null, // Default: \"authenticate\"\n *             enable_csrf?: bool|Param, // Default: false\n *             post_only?: bool|Param, // Default: true\n *             form_only?: bool|Param, // Default: false\n *             always_use_default_target_path?: bool|Param, // Default: false\n *             default_target_path?: scalar|Param|null, // Default: \"/\"\n *             target_path_parameter?: scalar|Param|null, // Default: \"_target_path\"\n *             use_referer?: bool|Param, // Default: false\n *             failure_path?: scalar|Param|null, // Default: null\n *             failure_forward?: bool|Param, // Default: false\n *             failure_path_parameter?: scalar|Param|null, // Default: \"_failure_path\"\n *             service?: scalar|Param|null, // Default: \"ldap\"\n *             dn_string?: scalar|Param|null, // Default: \"{user_identifier}\"\n *             query_string?: scalar|Param|null,\n *             search_dn?: scalar|Param|null, // Default: \"\"\n *             search_password?: scalar|Param|null, // Default: \"\"\n *         },\n *         json_login?: array{\n *             provider?: scalar|Param|null,\n *             remember_me?: bool|Param, // Default: true\n *             success_handler?: scalar|Param|null,\n *             failure_handler?: scalar|Param|null,\n *             check_path?: scalar|Param|null, // Default: \"/login_check\"\n *             use_forward?: bool|Param, // Default: false\n *             login_path?: scalar|Param|null, // Default: \"/login\"\n *             username_path?: scalar|Param|null, // Default: \"username\"\n *             password_path?: scalar|Param|null, // Default: \"password\"\n *         },\n *         json_login_ldap?: array{\n *             provider?: scalar|Param|null,\n *             remember_me?: bool|Param, // Default: true\n *             success_handler?: scalar|Param|null,\n *             failure_handler?: scalar|Param|null,\n *             check_path?: scalar|Param|null, // Default: \"/login_check\"\n *             use_forward?: bool|Param, // Default: false\n *             login_path?: scalar|Param|null, // Default: \"/login\"\n *             username_path?: scalar|Param|null, // Default: \"username\"\n *             password_path?: scalar|Param|null, // Default: \"password\"\n *             service?: scalar|Param|null, // Default: \"ldap\"\n *             dn_string?: scalar|Param|null, // Default: \"{user_identifier}\"\n *             query_string?: scalar|Param|null,\n *             search_dn?: scalar|Param|null, // Default: \"\"\n *             search_password?: scalar|Param|null, // Default: \"\"\n *         },\n *         access_token?: array{\n *             provider?: scalar|Param|null,\n *             remember_me?: bool|Param, // Default: true\n *             success_handler?: scalar|Param|null,\n *             failure_handler?: scalar|Param|null,\n *             realm?: scalar|Param|null, // Default: null\n *             token_extractors?: list<scalar|Param|null>,\n *             token_handler?: string|array{\n *                 id?: scalar|Param|null,\n *                 oidc_user_info?: string|array{\n *                     base_uri?: scalar|Param|null, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require \"discovery\" to be configured).\n *                     discovery?: array{ // Enable the OIDC discovery.\n *                         cache?: array{\n *                             id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration.\n *                         },\n *                     },\n *                     claim?: scalar|Param|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: \"sub\"\n *                     client?: scalar|Param|null, // HttpClient service id to use to call the OIDC server.\n *                 },\n *                 oidc?: array{\n *                     discovery?: array{ // Enable the OIDC discovery.\n *                         base_uri?: list<scalar|Param|null>,\n *                         cache?: array{\n *                             id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration.\n *                         },\n *                     },\n *                     claim?: scalar|Param|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: \"sub\"\n *                     audience?: scalar|Param|null, // Audience set in the token, for validation purpose.\n *                     issuers?: list<scalar|Param|null>,\n *                     algorithm?: array<mixed>,\n *                     algorithms?: list<scalar|Param|null>,\n *                     key?: scalar|Param|null, // Deprecated: The \"key\" option is deprecated and will be removed in 8.0. Use the \"keyset\" option instead. // JSON-encoded JWK used to sign the token (must contain a \"kty\" key).\n *                     keyset?: scalar|Param|null, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys).\n *                     encryption?: bool|array{\n *                         enabled?: bool|Param, // Default: false\n *                         enforce?: bool|Param, // When enabled, the token shall be encrypted. // Default: false\n *                         algorithms?: list<scalar|Param|null>,\n *                         keyset?: scalar|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys).\n *                     },\n *                 },\n *                 cas?: array{\n *                     validation_url?: scalar|Param|null, // CAS server validation URL\n *                     prefix?: scalar|Param|null, // CAS prefix // Default: \"cas\"\n *                     http_client?: scalar|Param|null, // HTTP Client service // Default: null\n *                 },\n *                 oauth2?: scalar|Param|null,\n *             },\n *         },\n *         http_basic?: array{\n *             provider?: scalar|Param|null,\n *             realm?: scalar|Param|null, // Default: \"Secured Area\"\n *         },\n *         http_basic_ldap?: array{\n *             provider?: scalar|Param|null,\n *             realm?: scalar|Param|null, // Default: \"Secured Area\"\n *             service?: scalar|Param|null, // Default: \"ldap\"\n *             dn_string?: scalar|Param|null, // Default: \"{user_identifier}\"\n *             query_string?: scalar|Param|null,\n *             search_dn?: scalar|Param|null, // Default: \"\"\n *             search_password?: scalar|Param|null, // Default: \"\"\n *         },\n *         remember_me?: array{\n *             secret?: scalar|Param|null, // Default: \"%kernel.secret%\"\n *             service?: scalar|Param|null,\n *             user_providers?: list<scalar|Param|null>,\n *             catch_exceptions?: bool|Param, // Default: true\n *             signature_properties?: list<scalar|Param|null>,\n *             token_provider?: string|array{\n *                 service?: scalar|Param|null, // The service ID of a custom remember-me token provider.\n *                 doctrine?: bool|array{\n *                     enabled?: bool|Param, // Default: false\n *                     connection?: scalar|Param|null, // Default: null\n *                 },\n *             },\n *             token_verifier?: scalar|Param|null, // The service ID of a custom rememberme token verifier.\n *             name?: scalar|Param|null, // Default: \"REMEMBERME\"\n *             lifetime?: int|Param, // Default: 31536000\n *             path?: scalar|Param|null, // Default: \"/\"\n *             domain?: scalar|Param|null, // Default: null\n *             secure?: true|false|\"auto\"|Param, // Default: null\n *             httponly?: bool|Param, // Default: true\n *             samesite?: null|\"lax\"|\"strict\"|\"none\"|Param, // Default: \"lax\"\n *             always_remember_me?: bool|Param, // Default: false\n *             remember_me_parameter?: scalar|Param|null, // Default: \"_remember_me\"\n *         },\n *     }>,\n *     access_control?: list<array{ // Default: []\n *         request_matcher?: scalar|Param|null, // Default: null\n *         requires_channel?: scalar|Param|null, // Default: null\n *         path?: scalar|Param|null, // Use the urldecoded format. // Default: null\n *         host?: scalar|Param|null, // Default: null\n *         port?: int|Param, // Default: null\n *         ips?: list<scalar|Param|null>,\n *         attributes?: array<string, scalar|Param|null>,\n *         route?: scalar|Param|null, // Default: null\n *         methods?: list<scalar|Param|null>,\n *         allow_if?: scalar|Param|null, // Default: null\n *         roles?: list<scalar|Param|null>,\n *     }>,\n *     role_hierarchy?: array<string, string|list<scalar|Param|null>>,\n * }\n * @psalm-type TwigConfig = array{\n *     form_themes?: list<scalar|Param|null>,\n *     globals?: array<string, array{ // Default: []\n *         id?: scalar|Param|null,\n *         type?: scalar|Param|null,\n *         value?: mixed,\n *     }>,\n *     autoescape_service?: scalar|Param|null, // Default: null\n *     autoescape_service_method?: scalar|Param|null, // Default: null\n *     base_template_class?: scalar|Param|null, // Deprecated: The child node \"base_template_class\" at path \"twig.base_template_class\" is deprecated.\n *     cache?: scalar|Param|null, // Default: true\n *     charset?: scalar|Param|null, // Default: \"%kernel.charset%\"\n *     debug?: bool|Param, // Default: \"%kernel.debug%\"\n *     strict_variables?: bool|Param, // Default: \"%kernel.debug%\"\n *     auto_reload?: scalar|Param|null,\n *     optimizations?: int|Param,\n *     default_path?: scalar|Param|null, // The default path used to load templates. // Default: \"%kernel.project_dir%/templates\"\n *     file_name_pattern?: list<scalar|Param|null>,\n *     paths?: array<string, mixed>,\n *     date?: array{ // The default format options used by the date filter.\n *         format?: scalar|Param|null, // Default: \"F j, Y H:i\"\n *         interval_format?: scalar|Param|null, // Default: \"%d days\"\n *         timezone?: scalar|Param|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null\n *     },\n *     number_format?: array{ // The default format options for the number_format filter.\n *         decimals?: int|Param, // Default: 0\n *         decimal_point?: scalar|Param|null, // Default: \".\"\n *         thousands_separator?: scalar|Param|null, // Default: \",\"\n *     },\n *     mailer?: array{\n *         html_to_text_converter?: scalar|Param|null, // A service implementing the \"Symfony\\Component\\Mime\\HtmlToTextConverter\\HtmlToTextConverterInterface\". // Default: null\n *     },\n * }\n * @psalm-type WebProfilerConfig = array{\n *     toolbar?: bool|array{ // Profiler toolbar configuration\n *         enabled?: bool|Param, // Default: false\n *         ajax_replace?: bool|Param, // Replace toolbar on AJAX requests // Default: false\n *     },\n *     intercept_redirects?: bool|Param, // Default: false\n *     excluded_ajax_paths?: scalar|Param|null, // Default: \"^/((index|app(_[\\\\w]+)?)\\\\.php/)?_wdt\"\n * }\n * @psalm-type MonologConfig = array{\n *     use_microseconds?: scalar|Param|null, // Default: true\n *     channels?: list<scalar|Param|null>,\n *     handlers?: array<string, array{ // Default: []\n *         type?: scalar|Param|null,\n *         id?: scalar|Param|null,\n *         enabled?: bool|Param, // Default: true\n *         priority?: scalar|Param|null, // Default: 0\n *         level?: scalar|Param|null, // Default: \"DEBUG\"\n *         bubble?: bool|Param, // Default: true\n *         interactive_only?: bool|Param, // Default: false\n *         app_name?: scalar|Param|null, // Default: null\n *         fill_extra_context?: bool|Param, // Default: false\n *         include_stacktraces?: bool|Param, // Default: false\n *         process_psr_3_messages?: array{\n *             enabled?: bool|Param|null, // Default: null\n *             date_format?: scalar|Param|null,\n *             remove_used_context_fields?: bool|Param,\n *         },\n *         path?: scalar|Param|null, // Default: \"%kernel.logs_dir%/%kernel.environment%.log\"\n *         file_permission?: scalar|Param|null, // Default: null\n *         use_locking?: bool|Param, // Default: false\n *         filename_format?: scalar|Param|null, // Default: \"{filename}-{date}\"\n *         date_format?: scalar|Param|null, // Default: \"Y-m-d\"\n *         ident?: scalar|Param|null, // Default: false\n *         logopts?: scalar|Param|null, // Default: 1\n *         facility?: scalar|Param|null, // Default: \"user\"\n *         max_files?: scalar|Param|null, // Default: 0\n *         action_level?: scalar|Param|null, // Default: \"WARNING\"\n *         activation_strategy?: scalar|Param|null, // Default: null\n *         stop_buffering?: bool|Param, // Default: true\n *         passthru_level?: scalar|Param|null, // Default: null\n *         excluded_404s?: list<scalar|Param|null>,\n *         excluded_http_codes?: list<array{ // Default: []\n *             code?: scalar|Param|null,\n *             urls?: list<scalar|Param|null>,\n *         }>,\n *         accepted_levels?: list<scalar|Param|null>,\n *         min_level?: scalar|Param|null, // Default: \"DEBUG\"\n *         max_level?: scalar|Param|null, // Default: \"EMERGENCY\"\n *         buffer_size?: scalar|Param|null, // Default: 0\n *         flush_on_overflow?: bool|Param, // Default: false\n *         handler?: scalar|Param|null,\n *         url?: scalar|Param|null,\n *         exchange?: scalar|Param|null,\n *         exchange_name?: scalar|Param|null, // Default: \"log\"\n *         room?: scalar|Param|null,\n *         message_format?: scalar|Param|null, // Default: \"text\"\n *         api_version?: scalar|Param|null, // Default: null\n *         channel?: scalar|Param|null, // Default: null\n *         bot_name?: scalar|Param|null, // Default: \"Monolog\"\n *         use_attachment?: scalar|Param|null, // Default: true\n *         use_short_attachment?: scalar|Param|null, // Default: false\n *         include_extra?: scalar|Param|null, // Default: false\n *         icon_emoji?: scalar|Param|null, // Default: null\n *         webhook_url?: scalar|Param|null,\n *         exclude_fields?: list<scalar|Param|null>,\n *         team?: scalar|Param|null,\n *         notify?: scalar|Param|null, // Default: false\n *         nickname?: scalar|Param|null, // Default: \"Monolog\"\n *         token?: scalar|Param|null,\n *         region?: scalar|Param|null,\n *         source?: scalar|Param|null,\n *         use_ssl?: bool|Param, // Default: true\n *         user?: mixed,\n *         title?: scalar|Param|null, // Default: null\n *         host?: scalar|Param|null, // Default: null\n *         port?: scalar|Param|null, // Default: 514\n *         config?: list<scalar|Param|null>,\n *         members?: list<scalar|Param|null>,\n *         connection_string?: scalar|Param|null,\n *         timeout?: scalar|Param|null,\n *         time?: scalar|Param|null, // Default: 60\n *         deduplication_level?: scalar|Param|null, // Default: 400\n *         store?: scalar|Param|null, // Default: null\n *         connection_timeout?: scalar|Param|null,\n *         persistent?: bool|Param,\n *         dsn?: scalar|Param|null,\n *         hub_id?: scalar|Param|null, // Default: null\n *         client_id?: scalar|Param|null, // Default: null\n *         auto_log_stacks?: scalar|Param|null, // Default: false\n *         release?: scalar|Param|null, // Default: null\n *         environment?: scalar|Param|null, // Default: null\n *         message_type?: scalar|Param|null, // Default: 0\n *         parse_mode?: scalar|Param|null, // Default: null\n *         disable_webpage_preview?: bool|Param|null, // Default: null\n *         disable_notification?: bool|Param|null, // Default: null\n *         split_long_messages?: bool|Param, // Default: false\n *         delay_between_messages?: bool|Param, // Default: false\n *         topic?: int|Param, // Default: null\n *         factor?: int|Param, // Default: 1\n *         tags?: list<scalar|Param|null>,\n *         console_formater_options?: mixed, // Deprecated: \"monolog.handlers..console_formater_options.console_formater_options\" is deprecated, use \"monolog.handlers..console_formater_options.console_formatter_options\" instead.\n *         console_formatter_options?: mixed, // Default: []\n *         formatter?: scalar|Param|null,\n *         nested?: bool|Param, // Default: false\n *         publisher?: string|array{\n *             id?: scalar|Param|null,\n *             hostname?: scalar|Param|null,\n *             port?: scalar|Param|null, // Default: 12201\n *             chunk_size?: scalar|Param|null, // Default: 1420\n *             encoder?: \"json\"|\"compressed_json\"|Param,\n *         },\n *         mongo?: string|array{\n *             id?: scalar|Param|null,\n *             host?: scalar|Param|null,\n *             port?: scalar|Param|null, // Default: 27017\n *             user?: scalar|Param|null,\n *             pass?: scalar|Param|null,\n *             database?: scalar|Param|null, // Default: \"monolog\"\n *             collection?: scalar|Param|null, // Default: \"logs\"\n *         },\n *         mongodb?: string|array{\n *             id?: scalar|Param|null, // ID of a MongoDB\\Client service\n *             uri?: scalar|Param|null,\n *             username?: scalar|Param|null,\n *             password?: scalar|Param|null,\n *             database?: scalar|Param|null, // Default: \"monolog\"\n *             collection?: scalar|Param|null, // Default: \"logs\"\n *         },\n *         elasticsearch?: string|array{\n *             id?: scalar|Param|null,\n *             hosts?: list<scalar|Param|null>,\n *             host?: scalar|Param|null,\n *             port?: scalar|Param|null, // Default: 9200\n *             transport?: scalar|Param|null, // Default: \"Http\"\n *             user?: scalar|Param|null, // Default: null\n *             password?: scalar|Param|null, // Default: null\n *         },\n *         index?: scalar|Param|null, // Default: \"monolog\"\n *         document_type?: scalar|Param|null, // Default: \"logs\"\n *         ignore_error?: scalar|Param|null, // Default: false\n *         redis?: string|array{\n *             id?: scalar|Param|null,\n *             host?: scalar|Param|null,\n *             password?: scalar|Param|null, // Default: null\n *             port?: scalar|Param|null, // Default: 6379\n *             database?: scalar|Param|null, // Default: 0\n *             key_name?: scalar|Param|null, // Default: \"monolog_redis\"\n *         },\n *         predis?: string|array{\n *             id?: scalar|Param|null,\n *             host?: scalar|Param|null,\n *         },\n *         from_email?: scalar|Param|null,\n *         to_email?: list<scalar|Param|null>,\n *         subject?: scalar|Param|null,\n *         content_type?: scalar|Param|null, // Default: null\n *         headers?: list<scalar|Param|null>,\n *         mailer?: scalar|Param|null, // Default: null\n *         email_prototype?: string|array{\n *             id?: scalar|Param|null,\n *             method?: scalar|Param|null, // Default: null\n *         },\n *         lazy?: bool|Param, // Default: true\n *         verbosity_levels?: array{\n *             VERBOSITY_QUIET?: scalar|Param|null, // Default: \"ERROR\"\n *             VERBOSITY_NORMAL?: scalar|Param|null, // Default: \"WARNING\"\n *             VERBOSITY_VERBOSE?: scalar|Param|null, // Default: \"NOTICE\"\n *             VERBOSITY_VERY_VERBOSE?: scalar|Param|null, // Default: \"INFO\"\n *             VERBOSITY_DEBUG?: scalar|Param|null, // Default: \"DEBUG\"\n *         },\n *         channels?: string|array{\n *             type?: scalar|Param|null,\n *             elements?: list<scalar|Param|null>,\n *         },\n *     }>,\n * }\n * @psalm-type DebugConfig = array{\n *     max_items?: int|Param, // Max number of displayed items past the first level, -1 means no limit. // Default: 2500\n *     min_depth?: int|Param, // Minimum tree depth to clone all the items, 1 is default. // Default: 1\n *     max_string_length?: int|Param, // Max length of displayed strings, -1 means no limit. // Default: -1\n *     dump_destination?: scalar|Param|null, // A stream URL where dumps should be written to. // Default: null\n *     theme?: \"dark\"|\"light\"|Param, // Changes the color of the dump() output when rendered directly on the templating. \"dark\" (default) or \"light\". // Default: \"dark\"\n * }\n * @psalm-type MakerConfig = array{\n *     root_namespace?: scalar|Param|null, // Default: \"App\"\n *     generate_final_classes?: bool|Param, // Default: true\n *     generate_final_entities?: bool|Param, // Default: false\n * }\n * @psalm-type ConfigType = array{\n *     imports?: ImportsConfig,\n *     parameters?: ParametersConfig,\n *     services?: ServicesConfig,\n *     framework?: FrameworkConfig,\n *     doctrine?: DoctrineConfig,\n *     doctrine_migrations?: DoctrineMigrationsConfig,\n *     security?: SecurityConfig,\n *     twig?: TwigConfig,\n *     web_profiler?: WebProfilerConfig,\n *     monolog?: MonologConfig,\n *     debug?: DebugConfig,\n *     \"when@dev\"?: array{\n *         imports?: ImportsConfig,\n *         parameters?: ParametersConfig,\n *         services?: ServicesConfig,\n *         framework?: FrameworkConfig,\n *         doctrine?: DoctrineConfig,\n *         doctrine_migrations?: DoctrineMigrationsConfig,\n *         security?: SecurityConfig,\n *         twig?: TwigConfig,\n *         web_profiler?: WebProfilerConfig,\n *         monolog?: MonologConfig,\n *         debug?: DebugConfig,\n *         maker?: MakerConfig,\n *     },\n *     \"when@test\"?: array{\n *         imports?: ImportsConfig,\n *         parameters?: ParametersConfig,\n *         services?: ServicesConfig,\n *         framework?: FrameworkConfig,\n *         doctrine?: DoctrineConfig,\n *         doctrine_migrations?: DoctrineMigrationsConfig,\n *         security?: SecurityConfig,\n *         twig?: TwigConfig,\n *         web_profiler?: WebProfilerConfig,\n *         monolog?: MonologConfig,\n *         debug?: DebugConfig,\n *     },\n *     ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias\n *         imports?: ImportsConfig,\n *         parameters?: ParametersConfig,\n *         services?: ServicesConfig,\n *         ...<string, ExtensionType>,\n *     }>\n * }\n */\nfinal class App\n{\n    /**\n     * @param ConfigType $config\n     *\n     * @psalm-return ConfigType\n     */\n    public static function config(array $config): array\n    {\n        /** @var ConfigType $config */\n        $config = AppReference::config($config);\n\n        return $config;\n    }\n}\n\nnamespace Symfony\\Component\\Routing\\Loader\\Configurator;\n\n/**\n * This class provides array-shapes for configuring the routes of an application.\n *\n * Example:\n *\n *     ```php\n *     // config/routes.php\n *     namespace Symfony\\Component\\Routing\\Loader\\Configurator;\n *\n *     return Routes::config([\n *         'controllers' => [\n *             'resource' => 'routing.controllers',\n *         ],\n *     ]);\n *     ```\n *\n * @psalm-type RouteConfig = array{\n *     path: string|array<string,string>,\n *     controller?: string,\n *     methods?: string|list<string>,\n *     requirements?: array<string,string>,\n *     defaults?: array<string,mixed>,\n *     options?: array<string,mixed>,\n *     host?: string|array<string,string>,\n *     schemes?: string|list<string>,\n *     condition?: string,\n *     locale?: string,\n *     format?: string,\n *     utf8?: bool,\n *     stateless?: bool,\n * }\n * @psalm-type ImportConfig = array{\n *     resource: string,\n *     type?: string,\n *     exclude?: string|list<string>,\n *     prefix?: string|array<string,string>,\n *     name_prefix?: string,\n *     trailing_slash_on_root?: bool,\n *     controller?: string,\n *     methods?: string|list<string>,\n *     requirements?: array<string,string>,\n *     defaults?: array<string,mixed>,\n *     options?: array<string,mixed>,\n *     host?: string|array<string,string>,\n *     schemes?: string|list<string>,\n *     condition?: string,\n *     locale?: string,\n *     format?: string,\n *     utf8?: bool,\n *     stateless?: bool,\n * }\n * @psalm-type AliasConfig = array{\n *     alias: string,\n *     deprecated?: array{package:string, version:string, message?:string},\n * }\n * @psalm-type RoutesConfig = array{\n *     \"when@dev\"?: array<string, RouteConfig|ImportConfig|AliasConfig>,\n *     \"when@test\"?: array<string, RouteConfig|ImportConfig|AliasConfig>,\n *     ...<string, RouteConfig|ImportConfig|AliasConfig>\n * }\n */\nfinal class Routes\n{\n    /**\n     * @param RoutesConfig $config\n     *\n     * @psalm-return RoutesConfig\n     */\n    public static function config(array $config): array\n    {\n        return $config;\n    }\n}\n"
  },
  {
    "path": "config/routes/attributes.yaml",
    "content": "controllers:\n    resource: ../../src/Controller/\n    type: attribute\n\nkernel:\n    resource: App\\Kernel\n    type: attribute\n"
  },
  {
    "path": "config/routes/dev/framework.yaml",
    "content": "_errors:\n    resource: '@FrameworkBundle/Resources/config/routing/errors.php'\n    prefix: /_error"
  },
  {
    "path": "config/routes/dev/web_profiler.yaml",
    "content": "web_profiler_wdt:\n    resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'\n    prefix: /_wdt\n\nweb_profiler_profiler:\n    resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'\n    prefix: /_profiler"
  },
  {
    "path": "config/routes.yaml",
    "content": "#index:\n#    path: /\n#    controller: App\\Controller\\DefaultController::index\n"
  },
  {
    "path": "config/services.yaml",
    "content": "# This file is the entry point to configure your own services.\n# Files in the packages/ subdirectory configure your dependencies.\n\n# Put parameters here that don't need to change on each machine where the app is deployed\n# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration\nparameters:\n    default_database_driver: \"mysql\"\n    default_admin_auth_bypass: \"false\"\n    timezone: '%env(APP_TIMEZONE)%'\n    public_calendars_enabled: '%env(default:default_public_calendars_enabled:bool:PUBLIC_CALENDARS_ENABLED)%'\n    default_public_calendars_enabled: \"true\"\n    birthday_reminder_offset: '%env(default:default_birthday_reminder_offset:BIRTHDAY_REMINDER_OFFSET)%'\n    default_birthday_reminder_offset: \"PT9H\"\n    caldav_enabled: \"%env(bool:CALDAV_ENABLED)%\"\n    carddav_enabled: \"%env(bool:CARDDAV_ENABLED)%\"\n\nservices:\n    # default configuration for services in *this* file\n    _defaults:\n        autowire: true      # Automatically injects dependencies in your services.\n        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.\n\n    # makes classes in src/ available to be used as services\n    # this creates a service per class whose id is the fully-qualified class name\n    App\\:\n        resource: '../src/*'\n        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'\n\n    App\\Services\\Utils:\n        arguments:\n            $authRealm: \"%env(AUTH_REALM)%\"\n\n    App\\Services\\IMAPAuth:\n        arguments:\n            $IMAPAuthUrl: \"%env(IMAP_AUTH_URL)%\"\n            $IMAPEncryptionMethod: \"%env(IMAP_ENCRYPTION_METHOD)%\"\n            $IMAPCertificateValidation: \"%env(bool:IMAP_CERTIFICATE_VALIDATION)%\"\n            $autoCreate: \"%env(bool:IMAP_AUTH_USER_AUTOCREATE)%\"\n\n    App\\Services\\LDAPAuth:\n        arguments:\n            $LDAPAuthUrl: \"%env(LDAP_AUTH_URL)%\"\n            $LDAPDnPattern: \"%env(LDAP_DN_PATTERN)%\"\n            $LDAPMailAttribute: \"%env(LDAP_MAIL_ATTRIBUTE)%\"\n            $LDAPCertificateCheckingStrategy: \"%env(LDAP_CERTIFICATE_CHECKING_STRATEGY)%\"\n            $autoCreate: \"%env(bool:LDAP_AUTH_USER_AUTOCREATE)%\"\n\n    # controllers are imported separately to make sure services can be injected\n    # as action arguments even if you don't extend any base controller class\n    App\\Controller\\:\n        resource: '../src/Controller'\n        tags: ['controller.service_arguments']\n\n    App\\Controller\\DAVController:\n        arguments:\n            $publicDir: \"%kernel.project_dir%/public\"\n            $calDAVEnabled: \"%env(bool:CALDAV_ENABLED)%\"\n            $cardDAVEnabled: \"%env(bool:CARDDAV_ENABLED)%\"\n            $webDAVEnabled: \"%env(bool:WEBDAV_ENABLED)%\"\n            $publicCalendarsEnabled: \"%public_calendars_enabled%\"\n            $inviteAddress: \"%env(INVITE_FROM_ADDRESS)%\"\n            $authMethod: \"%env(AUTH_METHOD)%\"\n            $authRealm: \"%env(AUTH_REALM)%\"\n            $webdavPublicDir: \"%env(resolve:WEBDAV_PUBLIC_DIR)%\"\n            $webdavHomesDir: \"%env(resolve:WEBDAV_HOMES_DIR)%\"\n            $webdavTmpDir: \"%env(resolve:WEBDAV_TMP_DIR)%\"\n\n    App\\Security\\LoginFormAuthenticator:\n        arguments:\n            $adminLogin: \"%env(ADMIN_LOGIN)%\"\n            $adminPassword: \"%env(ADMIN_PASSWORD)%\"\n\n    App\\Logging\\Monolog\\PasswordFilterProcessor:\n        tags:\n            - { name: monolog.processor }\n\n    App\\Services\\BirthdayService:\n        arguments:\n            $birthdayReminderOffset: \"%birthday_reminder_offset%\"\n\n    App\\Security\\ApiKeyAuthenticator:\n        arguments:\n            $apiKey: \"%env(API_KEY)%\"\n\nwhen@dev:\n    services:\n        Symfony\\Component\\HttpKernel\\Profiler\\Profiler: '@profiler'\n\nwhen@test:\n    services:\n        Symfony\\Component\\HttpKernel\\Profiler\\Profiler: '@profiler'\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n\n# Initial PHP image is available here:\n# https://github.com/docker-library/php/blob/master/8.2/alpine3.18/fpm/Dockerfile#L33\nARG fpm_user=82:82\n\n# Base image, used to build extensions and the final image ———————————————————————\nFROM php:8.3-fpm-alpine AS base-image\n\n# Run update, and gets basic packages and packages for runtime\nRUN apk --no-progress --update add --no-cache \\\n        curl unzip fcgi \\\n        # These are for php-intl\n        icu-libs \\\n        # This one for LDAP\n        libldap \\\n        # This one for IMAP\n        libzip \\\n        # These are for GD (map image in mail)\n        freetype \\\n        libjpeg-turbo \\\n        libpng \\\n        # This is for PostgreSQL\n        libpq\n\n# Build all extensions in a separate image ———————————————————————————————————————\nFROM base-image AS extension-builder\n\n# Install ALL build dependencies at once\n# hadolint ignore=SC2086\nRUN apk --update --virtual build-deps add --no-cache \\\n    # Intl support\n    icu-dev \\\n    # PDO: PostgreSQL\n    libpq-dev \\\n    # GD (map image in mail)\n    freetype-dev libjpeg-turbo-dev libpng-dev \\\n    # LDAP auth support\n    openldap-dev \\\n    # Zip lib for PHP-IMAP\n    libzip-dev \\\n    $PHPIZE_DEPS\n\n# Build ALL extensions in one RUN command to reduce layers\n# and allow parallel compilation for extensions\nRUN docker-php-ext-configure intl \\\n    && docker-php-ext-configure pdo_mysql --with-pdo-mysql=mysqlnd \\\n    && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \\\n    && docker-php-ext-configure gd --with-freetype \\\n    && docker-php-ext-configure ldap \\\n    && docker-php-ext-configure zip \\\n    && docker-php-ext-install -j\"$(nproc)\" \\\n        intl \\\n        pdo_mysql \\\n        pgsql \\\n        pdo_pgsql \\\n        gd \\\n        ldap \\\n        zip \\\n        opcache \\\n    && docker-php-ext-enable gd opcache \\\n    && apk del build-deps \\\n    && rm -rf /tmp/* /var/cache/apk/*\n\nCOPY ./docker/configurations/opcache.ini /usr/local/etc/php/conf.d/opcache.ini\n\n# Final image ————————————————————————————————————————————————————————————————————\nFROM base-image\n\nARG fpm_user=82:82\nENV FPM_USER=${fpm_user}\n\nENV PHP_OPCACHE_MEMORY_CONSUMPTION=\"256\" \\\n    PHP_OPCACHE_MAX_WASTED_PERCENTAGE=\"10\"\n\nLABEL org.opencontainers.image.authors=\"tchap@tchap.me\"\nLABEL org.opencontainers.image.url=\"https://github.com/tchapi/davis/pkgs/container/davis\"\nLABEL org.opencontainers.image.description=\"A simple, fully translatable admin interface for sabre/dav based on Symfony 7 and Bootstrap 5\"\n\n# Rapatriate built extensions\nCOPY --from=extension-builder /usr/local/etc/php/conf.d     /usr/local/etc/php/conf.d/\nCOPY --from=extension-builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions/\n\n# This is for Hadolint\nSHELL [\"/bin/ash\", \"-eo\", \"pipefail\", \"-c\"]\n\n# PHP-FPM healthcheck\nRUN set -xe && echo \"pm.status_path = /status\" >> /usr/local/etc/php-fpm.d/zz-docker.conf\nRUN curl https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/v0.5.0/php-fpm-healthcheck \\\n    -o /usr/local/bin/php-fpm-healthcheck -s \\\n    && chmod +x /usr/local/bin/php-fpm-healthcheck\n\n# Davis installation\nCOPY --chown=${FPM_USER} . /var/www/davis\nWORKDIR /var/www/davis\n\n# Install Composer 2, then dependencies, compress the rather big INTL package, and then cleanup (only useful when using --squash)\nRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer\nRUN APP_ENV=prod COMPOSER_ALLOW_SUPERUSER=1 composer install --no-ansi --no-dev --no-interaction --no-progress --prefer-dist --optimize-autoloader \\\n    && php ./vendor/symfony/intl/Resources/bin/compress \\\n    && rm -rf /var/www/davis/docker\n\nUSER $FPM_USER\n\nHEALTHCHECK --interval=30s --timeout=1s CMD php-fpm-healthcheck || exit 1\n"
  },
  {
    "path": "docker/Dockerfile-standalone",
    "content": "# Initial PHP image is available here:\n# https://github.com/docker-library/php/blob/master/8.2/alpine3.18/fpm/Dockerfile#L33\n\n# Base image, used to build extensions and the final image ———————————————————————\nFROM php:8.3-fpm-alpine AS base-image\n\n# Run update, and gets basic packages and packages for runtime\nRUN apk --no-progress --update add --no-cache \\\n        curl unzip \\\n        # These are for php-intl\n        icu-libs \\\n        # This one for LDAP\n        libldap \\\n        # This one for IMAP\n        libzip \\\n        # These are for GD (map image in mail)\n        freetype \\\n        libjpeg-turbo \\\n        libpng \\\n        # This is for PostgreSQL\n        libpq \\\n        # For the webserver and process manager\n        caddy supervisor\n\n# Build all extensions in a separate image ———————————————————————————————————————\nFROM base-image AS extension-builder\n\n# Install ALL build dependencies at once\n# hadolint ignore=SC2086\nRUN apk --update --virtual build-deps add --no-cache \\\n    # Intl support\n    icu-dev \\\n    # PDO: PostgreSQL\n    libpq-dev \\\n    # GD (map image in mail)\n    freetype-dev libjpeg-turbo-dev libpng-dev \\\n    # LDAP auth support\n    openldap-dev \\\n    # Zip lib for PHP-IMAP\n    libzip-dev \\\n    $PHPIZE_DEPS\n\n# Build ALL extensions in one RUN command to reduce layers\n# and allow parallel compilation for extensions\nRUN docker-php-ext-configure intl \\\n    && docker-php-ext-configure pdo_mysql --with-pdo-mysql=mysqlnd \\\n    && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \\\n    && docker-php-ext-configure gd --with-freetype \\\n    && docker-php-ext-configure ldap \\\n    && docker-php-ext-configure zip \\\n    && docker-php-ext-install -j\"$(nproc)\" \\\n        intl \\\n        pdo_mysql \\\n        pgsql \\\n        pdo_pgsql \\\n        gd \\\n        ldap \\\n        zip \\\n        opcache \\\n    && docker-php-ext-enable gd opcache \\\n    && apk del build-deps \\\n    && rm -rf /tmp/* /var/cache/apk/*\n\nCOPY ./docker/configurations/opcache.ini /usr/local/etc/php/conf.d/opcache.ini\n\n# Final image ————————————————————————————————————————————————————————————————————\nFROM base-image\n\nENV PHP_OPCACHE_MEMORY_CONSUMPTION=\"256\" \\\n    PHP_OPCACHE_MAX_WASTED_PERCENTAGE=\"10\"\n\nLABEL org.opencontainers.image.authors=\"tchap@tchap.me\"\nLABEL org.opencontainers.image.url=\"https://github.com/tchapi/davis/pkgs/container/davis-standalone\"\nLABEL org.opencontainers.image.description=\"A simple, fully translatable admin interface for sabre/dav based on Symfony 7 and Bootstrap 5 (Standalone version with reverse-proxy)\"\n\n# Rapatriate built extensions\nCOPY --from=extension-builder /usr/local/etc/php/conf.d     /usr/local/etc/php/conf.d/\nCOPY --from=extension-builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions/\n\n# Davis source\n# The app folder needs to be owned by www-data so PHP-fpm can execute files\nCOPY --chown=www-data:www-data . /var/www/davis\nWORKDIR /var/www/davis\n\n# This is for Hadolint\nSHELL [\"/bin/ash\", \"-eo\", \"pipefail\", \"-c\"]\n\n# Install Composer 2, then dependencies, compress the rather big INTL package\nRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer\nRUN APP_ENV=prod COMPOSER_ALLOW_SUPERUSER=1 composer install --no-ansi --no-dev --no-interaction --no-progress --prefer-dist --optimize-autoloader \\\n    && php ./vendor/symfony/intl/Resources/bin/compress\n\n# Caddy: web server\nRUN mkdir -p /var/log/caddy\nCOPY ./docker/configurations/Caddyfile /etc/caddy/Caddyfile\n\n# Supervisor: Process manager\nRUN mkdir -p /var/log/supervisor && mkdir -p /var/log/php-fpm\nCOPY ./docker/configurations/supervisord.conf /etc/supervisord.conf\n\n# We want to use sockets inside the container between Caddy and PHP-fpm\n# NOTE: Creating a custom zzzz-custom.conf overrides the www.conf setting (files are processed alphabetically)\nRUN mkdir /var/run/php-fpm && chown -R www-data:www-data /var/run/php-fpm \\\n    && { \\\n        echo '[www]'; \\\n        echo 'listen = /var/run/php-fpm/php-fpm.sock'; \\\n    } | tee /usr/local/etc/php-fpm.d/zzzz-custom-docker.conf\n\nRUN mkdir -p ./var/log ./var/cache && chown -R www-data:www-data ./var\n\n# Cleanup (only useful when using --squash)\nRUN docker-php-source delete && \\\n    rm -rf /var/www/davis/docker\n\nCMD [\"/usr/bin/supervisord\", \"-c\", \"/etc/supervisord.conf\"]\n\nHEALTHCHECK --interval=120s --timeout=10s --start-period=60s --retries=3 \\\n    CMD curl --fail http://localhost:9000 || exit 1 \n\n# It's the Caddy port, not the PHP-fpm one (as we use sockets)\nEXPOSE 9000"
  },
  {
    "path": "docker/configurations/Caddyfile",
    "content": "{\n        auto_https off\n}\n\n:9000 {\n\t# Redirect .well-known\n\tredir /.well-known/caldav  /dav/\n\tredir /.well-known/carddav /dav/\n\n        root * /var/www/davis/public\n        php_fastcgi unix//var/run/php-fpm/php-fpm.sock {\n                # Preserve the original X-Forwarded-Proto from upstream, as it might be HTTPS\n                header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto}\n                header_up X-Forwarded-Host {http.request.header.X-Forwarded-Host}\n                header_up X-Forwarded-For {http.request.header.X-Forwarded-For}\n        }\n\n        file_server {\n                # Safety net, just in case\n                hide .git .gitignore\n        }\n\n        # enable compression\n        encode zstd gzip\n\n        # Remove leaky headers\n        header {\n                -Server\n                -X-Powered-By\n\n                # keep referrer data off of HTTP connections\n                Referrer-Policy no-referrer-when-downgrade\n\n                # disable clients from sniffing the media type\n                X-Content-Type-Options nosniff\n        }\n\n}\n"
  },
  {
    "path": "docker/configurations/nginx.conf",
    "content": "# This is a very simple / naive configuration for nginx + Davis\n#\n# USE HTTPS IN PRODUCTION\n#\n\nupstream docker-davis {\n    server davis:9000;\n}\n\nserver {\n    listen 80;\n    access_log off;\n\n    root /var/www/davis/public/;\n    index index.php;\n\n    rewrite ^/.well-known/caldav /dav/ redirect;\n    rewrite ^/.well-known/carddav /dav/ redirect;\n\n    charset utf-8;\n\n    location ~ /(\\.ht) {\n        deny all;\n        return 404;\n    }\n\n    location / {\n        try_files $uri $uri/ /index.php$is_args$args;\n    }\n\n    location ~ ^(.+\\.php)(.*)$ {\n        try_files       $fastcgi_script_name =404;\n        include         fastcgi_params;\n        fastcgi_pass    docker-davis;\n        fastcgi_param   SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n        fastcgi_param   PATH_INFO        $fastcgi_path_info;\n        fastcgi_split_path_info  ^(.+\\.php)(.*)$;\n    }\n}\n"
  },
  {
    "path": "docker/configurations/opcache.ini",
    "content": "opcache.enable=1\nopcache.jit=1255\nopcache.jit_buffer_size=128M\nopcache.revalidate_freq=60\nopcache.save_comments=1\nopcache.validate_timestamps=1\nopcache.max_accelerated_files=32531\nopcache.memory_consumption=${PHP_OPCACHE_MEMORY_CONSUMPTION}\nopcache.max_wasted_percentage=${PHP_OPCACHE_MAX_WASTED_PERCENTAGE}\nopcache.interned_strings_buffer=64"
  },
  {
    "path": "docker/configurations/supervisord.conf",
    "content": "[supervisord]\nnodaemon=true\nuser=root\npidfile=/run/supervisord.pid\nlogfile=/dev/null\nlogfile_maxbytes=0\n\n[unix_http_server]\nfile=/run/supervisord.sock ; the path to the socket file\n\n[supervisorctl]\nserverurl=unix:///run/supervisord.sock ; use a unix:// URL for a unix socket\n\n[rpcinterface:supervisor]\nsupervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface\n\n[program:caddy]\ncommand=/usr/sbin/caddy run -c /etc/caddy/Caddyfile\nautostart=true\nautorestart=true\nredirect_stderr=true\nstdout_logfile=/var/log/caddy/access.log\nstdout_logfile_maxbytes = 0\n\n[program:php-fpm]\ncommand=/usr/local/sbin/php-fpm --nodaemonize\nautostart=true\nautorestart=true\nredirect_stderr=true\nstdout_logfile=/var/log/php-fpm/access.log\nstdout_logfile_maxbytes = 0"
  },
  {
    "path": "docker/docker-compose-postgresql.yml",
    "content": "version: \"3.7\"\nname: \"davis-docker\"\n\nservices:\n\n  nginx:\n    image: nginx:1.25-alpine\n    container_name: nginx\n    command: \"/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \\\"daemon off;\\\"'\"\n    depends_on:\n      - davis\n    volumes:\n      - davis_www:/var/www/davis\n      - type: bind\n        source: ./configurations/nginx.conf\n        target: /etc/nginx/conf.d/default.conf\n    ports:\n      - 9000:80\n\n  postgresql:\n    image: postgres:16-alpine\n    container_name: postgresql\n    environment:\n      - POSTGRES_PASSWORD=${DB_PASSWORD}\n      - POSTGRES_DB=${DB_DATABASE}\n      - POSTGRES_USER=${DB_USER}\n    volumes:\n      - database_pg:/var/lib/postgresql/data\n\n  davis:\n    build:\n      context: ../\n      dockerfile: ./docker/Dockerfile\n    image: davis:latest\n    # If you want to use a prebuilt image from Github\n    # image: ghcr.io/tchapi/davis:edge\n    container_name: davis\n    env_file: .env\n    environment:\n      - DATABASE_DRIVER=postgresql\n      - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@postgresql:5432/${DB_DATABASE}?serverVersion=15&charset=UTF-8\n      - MAILER_DSN=smtp://${MAIL_USERNAME}:${MAIL_PASSWORD}@${MAIL_HOST}:${MAIL_PORT}\n    depends_on:\n      - postgresql\n    volumes:\n      - davis_www:/var/www/davis\n\nvolumes:\n  davis_www:\n    name: davis_www\n  database_pg:\n    name: database_pg\n"
  },
  {
    "path": "docker/docker-compose-sqlite.yml",
    "content": "version: \"3.7\"\nname: \"davis-docker\"\n\nservices:\n\n  nginx:\n    image: nginx:1.25-alpine\n    container_name: nginx\n    command: \"/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \\\"daemon off;\\\"'\"\n    depends_on:\n      - davis\n    volumes:\n      - davis_www:/var/www/davis\n      - type: bind\n        source: ./configurations/nginx.conf\n        target: /etc/nginx/conf.d/default.conf\n    ports:\n      - 9000:80\n\n  davis:\n    build:\n      context: ../\n      dockerfile: ./docker/Dockerfile\n    image: davis:latest\n    # If you want to use a prebuilt image from Github\n    # image: ghcr.io/tchapi/davis:edge\n    container_name: davis\n    env_file: .env\n    environment:\n      - DATABASE_DRIVER=sqlite\n      - DATABASE_URL=sqlite:////data/davis-database.db # ⚠️ 4 slashes for an absolute path ⚠️ + no quotes (so Symfony can resolve it)\n      - MAILER_DSN=smtp://${MAIL_USERNAME}:${MAIL_PASSWORD}@${MAIL_HOST}:${MAIL_PORT}\n    volumes:\n      - davis_www:/var/www/davis\n      - davis_data:/data\n\nvolumes:\n  davis_www:\n    name: davis_www\n  davis_data:\n    name: davis_data\n"
  },
  {
    "path": "docker/docker-compose-standalone.yml",
    "content": "version: \"3.7\"\nname: \"davis-docker\"\n\nservices:\n\n  mysql:\n    image: mariadb:10.6.10\n    container_name: mysql\n    environment:\n      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}\n      - MYSQL_DATABASE=${DB_DATABASE}\n      - MYSQL_USER=${DB_USER}\n      - MYSQL_PASSWORD=${DB_PASSWORD}\n    volumes:\n      - database:/var/lib/mysql\n\n  davis:\n    build:\n      context: ../\n      dockerfile: ./docker/Dockerfile-standalone\n    image: davis:latest\n    # If you want to use a prebuilt image from Github\n    # image: ghcr.io/tchapi/davis-standalone:edge\n    container_name: davis-standalone\n    env_file: .env\n    environment:\n      - DATABASE_DRIVER=mysql\n      - DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_DATABASE}?serverVersion=mariadb-10.6.10&charset=utf8mb4\n      - MAILER_DSN=smtp://${MAIL_USERNAME}:${MAIL_PASSWORD}@${MAIL_HOST}:${MAIL_PORT}\n    depends_on:\n      - mysql\n    ports:\n      - 9000:9000\n\nvolumes:\n  database:\n    name: database\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "version: \"3.7\"\nname: \"davis-docker\"\n\nservices:\n\n  nginx:\n    image: nginx:1.25-alpine\n    container_name: nginx\n    command: \"/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \\\"daemon off;\\\"'\"\n    depends_on:\n      - davis\n    volumes:\n      - davis_www:/var/www/davis\n      - type: bind\n        source: ./configurations/nginx.conf\n        target: /etc/nginx/conf.d/default.conf\n    ports:\n      - 9000:80\n\n  mysql:\n    image: mariadb:10.11\n    container_name: mysql\n    environment:\n      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}\n      - MYSQL_DATABASE=${DB_DATABASE}\n      - MYSQL_USER=${DB_USER}\n      - MYSQL_PASSWORD=${DB_PASSWORD}\n    volumes:\n      - database:/var/lib/mysql\n\n  davis:\n    build:\n      context: ../\n      dockerfile: ./docker/Dockerfile\n      args:\n        fpm_user: 101:101\n    image: davis:latest\n    # If you want to use a prebuilt image from Github\n    # image: ghcr.io/tchapi/davis:edge\n    container_name: davis\n    env_file: .env\n    environment:\n      - DATABASE_DRIVER=mysql\n      - DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_DATABASE}?serverVersion=mariadb-10.6.10&charset=utf8mb4\n      - MAILER_DSN=smtp://${MAIL_USERNAME}:${MAIL_PASSWORD}@${MAIL_HOST}:${MAIL_PORT}\n    depends_on:\n      - mysql\n    volumes:\n      - davis_www:/var/www/davis\n\nvolumes:\n  davis_www:\n    name: davis_www\n  database:\n    name: database\n"
  },
  {
    "path": "docs/api/README.md",
    "content": "# Davis API\n\n## API Version 1\n\n### Open Endpoints\n\nOpen endpoints require no Authentication.\n\n* [Health](v1/health.md) : `GET /api/v1/health`\n\n### Endpoints that require Authentication\n\nClosed endpoints require a valid `X-Davis-API-Token` to be included in the header of the request. Token needs to be configured in .env file (as a environment variable `API_KEY`) and can be generated using `php bin/console api:generate` command.\n\nWhen `API_KEY` is not set, the API endpoints are disabled and will return a 500 error if accessed.\n\n#### User related\n\nEach endpoint displays information related to the User:\n\n* [Get Users](v1/users/all.md) : `GET /api/v1/users`\n* [Get User Details](v1/users/details.md) : `GET /api/v1/users/:user_id`\n\n#### Calendars related\n\nEndpoints for viewing and modifying user calendars.\n\n* [Show All User Calendars](v1/calendars/all.md) : `GET /api/v1/calendars/:user_id`\n* [Show User Calendar Details](v1/calendars/details.md) : `GET /api/v1/calendars/:user_id/:calendar_id`\n* [Create User Calendar](v1/calendars/create.md) : `POST /api/v1/calendars/:user_id/create`\n* [Edit User Calendar](v1/calendars/edit.md) : `PUT /api/v1/calendars/:user_id/:calendar_id`\n* [Delete User Calendar](v1/calendars/delete.md) : `DELETE /api/v1/calendars/:user_id/:calendar_id`\n* [Show User Calendar Shares](v1/calendars/shares.md) : `GET /api/v1/calendars/:user_id/shares/:calendar_id`\n* [Share User Calendar](v1/calendars/share_add.md) : `POST /api/v1/calendars/:user_id/share/:calendar_id/add`\n* [Remove Share User Calendar](v1/calendars/share_remove.md) : `POST /api/v1/calendars/:user_id/share/:calendar_id/remove`\n"
  },
  {
    "path": "docs/api/v1/calendars/all.md",
    "content": "# User Calendars\n\nGets a list of all available calendars for a specific user.\n\n**URL** : `/api/v1/calendars/:user_id`\n\n**Method** : `GET`\n\n**Auth required** : YES\n\n**Params constraints**\n\n```\n:user_id -> \"[user id as an int]\",\n```\n\n**URL example**\n\n```json\n/api/v1/calendars/jdoe\n```\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Notes**: The `events`, `notes`, and `tasks` fields return a count (number) if the component is enabled for the calendar, or `null` if the component is disabled.\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"data\": {\n\t\t\"user_calendars\": [\n\t\t\t{\n\t\t\t\t\"id\": 1,\n\t\t\t\t\"uri\": \"default\",\n\t\t\t\t\"displayname\": \"Default Calendar\",\n\t\t\t\t\"events\": 0,\n\t\t\t\t\"notes\": null,\n\t\t\t\t\"tasks\": null\n\t\t\t}\n\t\t],\n\t\t\"shared_calendars\": [\n\t\t\t{\n\t\t\t\t\"id\": 10,\n\t\t\t\t\"uri\": \"c2152eb0-ada1-451f-bf33-b4a9571ec92e\",\n\t\t\t\t\"displayname\": \"Default Calendar\",\n\t\t\t\t\"events\": 0,\n\t\t\t\t\"notes\": null,\n\t\t\t\t\"tasks\": null\n\t\t\t}\n\t\t],\n\t\t\"subscriptions\": []\n\t},\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nShown when user does not have calendars:\n```json\n{\n\t\"status\": \"success\",\n\t\"data\": {\n\t\t\"user_calendars\": [],\n\t\t\"shared_calendars\": [],\n\t\t\"subscriptions\": []\n\t},\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If user is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\", \n    \"message\": \"User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```"
  },
  {
    "path": "docs/api/v1/calendars/create.md",
    "content": "# Create User Calendar\n\nCreates a new calendar for a specific user.\n\n**URL** : `/api/v1/calendars/:user_id/create`\n\n**Method** : `POST`\n\n**Auth required** : YES\n\n**Params constraints**\n\n```\n:user_id -> \"[user id as an int]\",\n```\n\n**Request Body constraints**\n\n```json\n{\n\t\"name\": \"[string: calendar name, alphanumeric, spaces, underscores and hyphens, max 64 chars]\",\n\t\"uri\": \"[string: calendar URI, lowercase alphanumeric, underscores and hyphens, max 128 chars]\",\n\t\"description\": \"[string: calendar description, alphanumeric, spaces, underscores and hyphens, max 256 chars, optional]\",\n\t\"events_support\": \"[string: 'true' or 'false', default 'true', optional]\",\n\t\"notes_support\": \"[string: 'true' or 'false', default 'false', optional]\",\n\t\"tasks_support\": \"[string: 'true' or 'false', default 'false', optional]\"\n}\n```\n\n**URL example**\n\n```\n/api/v1/calendars/jdoe/create\n```\n\n**Body example**\n\n```json\n{\n\t\"name\": \"Work Calendar\",\n\t\"uri\": \"work-calendar\",\n\t\"description\": \"Calendar for work events\",\n\t\"events_support\": \"true\",\n\t\"notes_support\": \"false\",\n\t\"tasks_support\": \"true\"\n}\n```\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"data\": {\n\t\t\"calendar_id\": 5,\n\t\t\"calendar_uri\": \"work-calendar\"\n\t},\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If user is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If request body contains invalid JSON.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid JSON\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If 'name' parameter is invalid (not matching the regex or exceeds length).\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Calendar Name\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If 'uri' parameter is invalid (not matching the regex or exceeds length).\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Calendar URI\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If calendar with specified URI already exists for the user.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Calendar URI Already Exists\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If 'description' parameter is invalid (not matching the regex or exceeds length).\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Calendar Description\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If no calendar components are enabled (all of `events_support`, `notes_support`, and `tasks_support` are false).\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"At least one calendar component must be enabled (events, notes, or tasks)\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n"
  },
  {
    "path": "docs/api/v1/calendars/delete.md",
    "content": "# Delete User Calendar\n\nDeletes a specific calendar for a specific user.\n\n**URL** : `/api/v1/calendars/:user_id/:calendar_id`\n\n**Method** : `DELETE`\n\n**Auth required** : YES\n\n**Params constraints**\n\n```\n:user_id -> \"[user id as an int]\",\n:calendar_id -> \"[numeric id of a calendar owned by the user]\",\n```\n\n**URL example**\n\n```\n/api/v1/calendars/jdoe/1\n```\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If user is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If ':calendar_id' is not owned by the specified ':username' or calendar instance is not found.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Instance Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If an error occurs while deleting the calendar.\n\n**Code** : `500 INTERNAL SERVER ERROR`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Failed to Delete Calendar\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n"
  },
  {
    "path": "docs/api/v1/calendars/details.md",
    "content": "# User Calendar Details\n\nGets a list of all available calendars for a specific user.\n\n**URL** : `/api/v1/calendars/:user_id/:calendar_id`\n\n**Method** : `GET`\n\n**Auth required** : YES\n\n**Params constraints**\n\n```\n:user_id -> \"[user id as an int]\",\n:calendar_id -> \"[numeric id of a calendar owned by the user]\",\n```\n\n**URL example**\n\n```json\n/api/v1/calendars/jdoe/1\n```\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"data\": {\n\t\t\"id\": 1,\n\t\t\"uri\": \"default\",\n\t\t\"displayname\": \"Default Calendar\",\n\t\t\"description\": \"Default Calendar for Joe Doe\",\n\t\t\"events\": {\n\t\t\t\"enabled\": true,\n\t\t\t\"count\": 0\n\t\t},\n\t\t\"notes\": {\n\t\t\t\"enabled\": false,\n\t\t\t\"count\": 0\n\t\t},\n\t\t\"tasks\": {\n\t\t\t\"enabled\": false,\n\t\t\t\"count\": 0\n\t\t}\n\t},\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nShown when user has no calendars with the given id:\n```json\n{\n\t\"status\": \"success\",\n\t\"data\": {},\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```"
  },
  {
    "path": "docs/api/v1/calendars/edit.md",
    "content": "# Edit User Calendar\n\nEdits an existing calendar for a specific user.\n\n**URL** : `/api/v1/calendars/:user_id/:calendar_id`\n\n**Method** : `PUT` or `PATCH`\n\n**Auth required** : YES\n\n**Params constraints**\n\n```\n:user_id -> \"[user id as an int]\",\n:calendar_id -> \"[numeric id of a calendar owned by the user]\",\n```\n\n**Request Body constraints**\n\n```json\n{\n\t\"name\": \"[string: calendar name, alphanumeric, spaces, underscores and hyphens, max 64 chars]\",\n\t\"description\": \"[string: calendar description, alphanumeric, spaces, underscores and hyphens, max 256 chars, optional]\",\n\t\"events_support\": \"[string: 'true' or 'false', default 'true', optional]\",\n\t\"notes_support\": \"[string: 'true' or 'false', default 'false', optional]\",\n\t\"tasks_support\": \"[string: 'true' or 'false', default 'false', optional]\"\n}\n```\n\n**URL example**\n\n```\n/api/v1/calendars/jdoe/1\n```\n\n**Body example**\n\n```json\n{\n\t\"name\": \"Updated Work Calendar\",\n\t\"description\": \"Updated calendar for work events\",\n\t\"events_support\": \"true\",\n\t\"notes_support\": \"true\",\n\t\"tasks_support\": \"false\"\n}\n```\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If user is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If request body contains invalid JSON.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid JSON\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If ':calendar_id' is not owned by the specified ':username'.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Calendar ID\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If calendar instance is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Calendar Instance Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If 'name' parameter is invalid (not matching the regex or exceeds length).\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Calendar Name\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If 'description' parameter is invalid (not matching the regex or exceeds length).\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Calendar Description\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If no calendar components are enabled (all of `events_support`, `notes_support`, and `tasks_support` are false).\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"At least one calendar component must be enabled (events, notes, or tasks)\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```"
  },
  {
    "path": "docs/api/v1/calendars/share_add.md",
    "content": "# Share User Calendar\n\nShares (or updates write access) a calendar owned by the specified user to another user.\n\n**URL** : `/api/v1/calendars/:user_id/share/:calendar_id/add`\n\n**Method** : `POST`\n\n**Auth required** : YES\n\n**Params constraints**\n\n```\n:user_id -> \"[user id as an int]\",\n:calendar_id -> \"[numeric id of a calendar owned by the user]\",\n```\n\n** Request Body constraints**\n```json\n{\n\t\"username\": \"[username of the user to add/update access]\",\n\t\"write_access\": \"[boolean: true to grant write access, false for read-only]\"\n}\n```\n\n**URL example**\n\n```json\n/api/v1/calendars/mdoe/share/1/add\n```\n\n**Body example**\n\n```json\n{\n\t\"username\": \"jdoe\",\n\t\"write_access\": true\n}\n```\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If user is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If request body contains invalid JSON.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid JSON\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If ':calendar_id' is not owned by the specified ':username'.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Calendar ID and User ID\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If 'username' is not valid or 'write_access' is not 'true' or 'false'.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Sharee ID/Write Access Value\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If calendar instance or user to share with is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\", \n    \"message\": \"Calendar Instance/User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```"
  },
  {
    "path": "docs/api/v1/calendars/share_remove.md",
    "content": "# Remove Share User Calendar\n\nRemoves access to a specific shared calendar for a specific user.\n\n**URL** : `/api/v1/calendars/:user_id/share/:calendar_id/remove`\n\n**Method** : `POST`\n\n**Auth required** : YES\n\n**Params constraints**\n\n```\n:user_id -> \"[user id as an int]\",\n:calendar_id -> \"[numeric id of a calendar owned by the user]\",\n```\n\n** Request Body Constraints**\n```json\n{\n\t\"username\": \"[username of the user to remove access]\"\n}\n```\n\n**URL example**\n\n```json\n/api/v1/calendars/mdoe/share/1/remove\n```\n\n**Body example**\n\n```json\n{\n\t\"username\": \"jdoe\"\n}\n```\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If user is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If request body contains invalid JSON.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid JSON\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If ':calendar_id' is not owned by the specified ':username'.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Calendar ID\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If 'username' is not valid.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Username\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If calendar instance or user to remove is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\", \n    \"message\": \"Calendar Instance/User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```"
  },
  {
    "path": "docs/api/v1/calendars/shares.md",
    "content": "# User Calendar Shares\n\nGets a list of all users with whom a specific user calendar is shared.\n\n**URL** : `/api/v1/calendars/:user_id/shares/:calendar_id`\n\n**Method** : `GET`\n\n**Auth required** : YES\n\n**Params constraints**\n\n```\n:user_id -> \"[user id as an int]\",\n:calendar_id -> \"[numeric id of a calendar owned by the user]\",\n```\n\n**URL example**\n\n```json\n/api/v1/calendars/mdoe/shares/1\n```\n\n**Important Note** : The `:calendar_id` must be a calendar instance owned by the user. The endpoint retrieves shares of the underlying Calendar entity, ensuring shares are found correctly regardless of the instance reference.\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"data\": [\n\t\t{\n\t\t\t\"username\": \"adoe\",\n\t\t\t\"user_id\": 5,\n\t\t\t\"principal_id\": 9,\n\t\t\t\"displayname\": \"Aiden Doe\",\n\t\t\t\"email\": \"adoe@example.org\",\n\t\t\t\"write_access\": false\n\t\t},\n\t\t{\n\t\t\t\"username\": \"jdoe\",\n\t\t\t\"user_id\": 4,\n\t\t\t\"principal_id\": 3,\n\t\t\t\"displayname\": \"John Doe\",\n\t\t\t\"email\": \"jdoe@example.org\",\n\t\t\t\"write_access\": true\n\t\t}\n\t]\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If user is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If ':calendar_id' and ':username' combination is invalid.\n\n**Code** : `400 BAD REQUEST`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"Invalid Calendar ID/Username\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n"
  },
  {
    "path": "docs/api/v1/health.md",
    "content": "# Health\n\nUsed to check if the API endpoint is active.\n\n**URL** : `/api/v1/health`\n\n**Method** : `GET`\n\n**Auth required** : NO\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content example**\n\n```json\n{\n\t\"status\": \"OK\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n"
  },
  {
    "path": "docs/api/v1/users/all.md",
    "content": "# Get Users\n\nGets a list of all available users.\n\n**URL** : `/api/v1/users`\n\n**Method** : `GET`\n\n**Auth required** : YES\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"data\": [\n\t\t{\n\t\t\t\"user_id\": 1,\n\t\t\t\"principal_id\": 3,\n\t\t\t\"uri\": \"principals/jdoe\",\n\t\t\t\"username\": \"jdoe\"\n\t\t}\n\t],\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nShown when there are no users in Davis:\n```json\n{\n\t\"status\": \"success\",\n\t\"data\": [],\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n"
  },
  {
    "path": "docs/api/v1/users/details.md",
    "content": "# User Details\n\nGets details about a specific user account.\n\n**URL** : `/api/v1/users/:user_id`\n\n**Method** : `GET`\n\n**Auth required** : YES\n\n**Params constraints**\n\n```\n:user_id -> \"[user id as an int]\",\n```\n\n**URL example**\n\n```json\n/api/v1/users/jdoe\n```\n\n## Success Response\n\n**Code** : `200 OK`\n\n**Content examples**\n\n```json\n{\n\t\"status\": \"success\",\n\t\"data\": {\n\t\t\"user_id\": 1,\n\t\t\"principal_id\": 3,\n\t\t\"uri\": \"principals/jdoe\",\n\t\t\"username\": \"jdoe\",\n\t\t\"displayname\": \"John Doe\",\n\t\t\"email\": \"jdoe@example.org\"\n\t},\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n## Error Response\n\n**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.\n\n**Code** : `401 UNAUTHORIZED`\n\n**Content** :\n\n```json\n{\n\t\"message\": \"No API token provided\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\nor\n\n```json\n{\n\t\"message\": \"Invalid API token\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n```\n\n**Condition** : If user is not found.\n\n**Code** : `404 NOT FOUND`\n\n**Content** :\n\n```json\n{\n\t\"status\": \"error\",\n\t\"message\": \"User Not Found\",\n\t\"timestamp\": \"2026-01-23T15:01:33+01:00\"\n}\n"
  },
  {
    "path": "migrations/Version20191030113307.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Migration: Create all necessary sabre/dav tables.\n */\nfinal class Version20191030113307 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Create all necessary sabre/dav tables';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('CREATE TABLE cards (id INT AUTO_INCREMENT NOT NULL, addressbookid INT NOT NULL, carddata LONGBLOB DEFAULT NULL, uri VARBINARY(255) DEFAULT NULL, lastmodified INT DEFAULT NULL, etag VARBINARY(32) DEFAULT NULL, size INT NOT NULL, INDEX IDX_4C258FD8B26C2E9 (addressbookid), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE principals (id INT AUTO_INCREMENT NOT NULL, uri VARBINARY(255) NOT NULL, email VARBINARY(255) DEFAULT NULL, displayname VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE locks (id INT AUTO_INCREMENT NOT NULL, owner VARCHAR(255) DEFAULT NULL, timeout INT DEFAULT NULL, created INT DEFAULT NULL, token VARBINARY(255) DEFAULT NULL, scope SMALLINT DEFAULT NULL, depth SMALLINT DEFAULT NULL, uri VARBINARY(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE propertystorage (id INT AUTO_INCREMENT NOT NULL, path VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, valuetype INT DEFAULT NULL, value LONGTEXT DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE users (id INT AUTO_INCREMENT NOT NULL, username VARBINARY(255) NOT NULL, digesta1 VARBINARY(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE calendarobjects (id INT AUTO_INCREMENT NOT NULL, calendarid INT NOT NULL, calendardata LONGBLOB DEFAULT NULL, uri VARBINARY(255) DEFAULT NULL, lastmodified INT DEFAULT NULL, etag VARBINARY(255) DEFAULT NULL, size INT NOT NULL, componenttype VARBINARY(255) DEFAULT NULL, firstoccurence INT DEFAULT NULL, lastoccurence INT DEFAULT NULL, uid VARBINARY(255) DEFAULT NULL, INDEX IDX_E14F332CB8CB7204 (calendarid), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE addressbooks (id INT AUTO_INCREMENT NOT NULL, principaluri VARBINARY(255) NOT NULL, displayname VARCHAR(255) NOT NULL, uri VARBINARY(255) NOT NULL, description LONGTEXT NOT NULL, synctoken VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE calendarsubscriptions (id INT AUTO_INCREMENT NOT NULL, uri VARBINARY(255) NOT NULL, principaluri VARBINARY(255) NOT NULL, source LONGTEXT DEFAULT NULL, displayname VARCHAR(255) DEFAULT NULL, refreshrate VARCHAR(10) DEFAULT NULL, calendarorder INT NOT NULL, calendarcolor VARBINARY(10) DEFAULT NULL, striptodos SMALLINT DEFAULT NULL, stripalarms SMALLINT DEFAULT NULL, stripattachments SMALLINT DEFAULT NULL, lastmodified INT DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE schedulingobjects (id INT AUTO_INCREMENT NOT NULL, principaluri VARBINARY(255) DEFAULT NULL, calendardata LONGBLOB DEFAULT NULL, uri VARBINARY(255) DEFAULT NULL, lastmodified INT DEFAULT NULL, etag VARBINARY(255) DEFAULT NULL, size INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE calendarinstances (id INT AUTO_INCREMENT NOT NULL, calendarid INT NOT NULL, principaluri VARBINARY(255) DEFAULT NULL, access SMALLINT NOT NULL, displayname VARCHAR(255) DEFAULT NULL, uri VARBINARY(255) DEFAULT NULL, description LONGTEXT DEFAULT NULL, calendarorder INT NOT NULL, calendarcolor VARBINARY(10) DEFAULT NULL, timezone VARCHAR(255) DEFAULT NULL, transparent INT DEFAULT NULL, share_href VARBINARY(255) DEFAULT NULL, share_displayname VARCHAR(255) DEFAULT NULL, share_invitestatus INT NOT NULL, INDEX IDX_51856561B8CB7204 (calendarid), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE calendars (id INT AUTO_INCREMENT NOT NULL, synctoken VARCHAR(255) NOT NULL, components VARBINARY(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE calendarchanges (id INT AUTO_INCREMENT NOT NULL, calendarid INT NOT NULL, uri VARBINARY(255) NOT NULL, synctoken INT NOT NULL, operation SMALLINT NOT NULL, INDEX IDX_737547E2B8CB7204 (calendarid), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('CREATE TABLE addressbookchanges (id INT AUTO_INCREMENT NOT NULL, addressbookid INT NOT NULL, uri VARBINARY(255) NOT NULL, synctoken VARCHAR(255) NOT NULL, operation INT NOT NULL, INDEX IDX_EB122CD58B26C2E9 (addressbookid), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('ALTER TABLE cards ADD CONSTRAINT FK_4C258FD8B26C2E9 FOREIGN KEY (addressbookid) REFERENCES addressbooks (id)');\n        $this->addSql('ALTER TABLE calendarobjects ADD CONSTRAINT FK_E14F332CB8CB7204 FOREIGN KEY (calendarid) REFERENCES calendars (id)');\n        $this->addSql('ALTER TABLE calendarinstances ADD CONSTRAINT FK_51856561B8CB7204 FOREIGN KEY (calendarid) REFERENCES calendars (id)');\n        $this->addSql('ALTER TABLE calendarchanges ADD CONSTRAINT FK_737547E2B8CB7204 FOREIGN KEY (calendarid) REFERENCES calendars (id)');\n        $this->addSql('ALTER TABLE addressbookchanges ADD CONSTRAINT FK_EB122CD58B26C2E9 FOREIGN KEY (addressbookid) REFERENCES addressbooks (id)');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE cards DROP FOREIGN KEY FK_4C258FD8B26C2E9');\n        $this->addSql('ALTER TABLE addressbookchanges DROP FOREIGN KEY FK_EB122CD58B26C2E9');\n        $this->addSql('ALTER TABLE calendarobjects DROP FOREIGN KEY FK_E14F332CB8CB7204');\n        $this->addSql('ALTER TABLE calendarinstances DROP FOREIGN KEY FK_51856561B8CB7204');\n        $this->addSql('ALTER TABLE calendarchanges DROP FOREIGN KEY FK_737547E2B8CB7204');\n        $this->addSql('DROP TABLE cards');\n        $this->addSql('DROP TABLE principals');\n        $this->addSql('DROP TABLE locks');\n        $this->addSql('DROP TABLE propertystorage');\n        $this->addSql('DROP TABLE users');\n        $this->addSql('DROP TABLE calendarobjects');\n        $this->addSql('DROP TABLE addressbooks');\n        $this->addSql('DROP TABLE calendarsubscriptions');\n        $this->addSql('DROP TABLE schedulingobjects');\n        $this->addSql('DROP TABLE calendarinstances');\n        $this->addSql('DROP TABLE calendars');\n        $this->addSql('DROP TABLE calendarchanges');\n        $this->addSql('DROP TABLE addressbookchanges');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20191113170650.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Add missing groupmembers table, and delegates.\n */\nfinal class Version20191113170650 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Add missing groupmembers table for proxy/read and proxy/write, and add an info in the principals table to separate main principals from proxy.';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('CREATE TABLE groupmembers (principal_id INT NOT NULL, member_id INT NOT NULL, INDEX IDX_6F15EDAC474870EE (principal_id), INDEX IDX_6F15EDAC7597D3FE (member_id), PRIMARY KEY(principal_id, member_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');\n        $this->addSql('ALTER TABLE groupmembers ADD CONSTRAINT FK_6F15EDAC474870EE FOREIGN KEY (principal_id) REFERENCES principals (id)');\n        $this->addSql('ALTER TABLE groupmembers ADD CONSTRAINT FK_6F15EDAC7597D3FE FOREIGN KEY (member_id) REFERENCES principals (id)');\n        $this->addSql('ALTER TABLE principals ADD is_main TINYINT(1) NOT NULL');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('DROP TABLE groupmembers');\n        $this->addSql('ALTER TABLE principals DROP is_main');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20191125093508.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Add the isAdmin property on the principals.\n */\nfinal class Version20191125093508 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Add the isAdmin property on the principals';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE principals ADD is_admin TINYINT(1) NOT NULL');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE principals DROP is_admin');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20191202091507.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * The default access value and share_invitestatus value should be set on the DB level, and timezone should be a text column.\n */\nfinal class Version20191202091507 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'The default access value and share_invitestatus value should be set on the DB level, and timezone should be a text column.';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE calendarinstances CHANGE access access SMALLINT DEFAULT 1 NOT NULL, CHANGE share_invitestatus share_invitestatus INT DEFAULT 2 NOT NULL, CHANGE timezone timezone LONGTEXT DEFAULT NULL');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE calendarinstances CHANGE access access SMALLINT NOT NULL, CHANGE share_invitestatus share_invitestatus INT NOT NULL, CHANGE timezone timezone LONGTEXT DEFAULT NULL, CHANGE timezone timezone VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT \\'NULL\\' COLLATE `utf8mb4_unicode_ci`');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20191203111729.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Make Address book's description nullable.\n */\nfinal class Version20191203111729 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Make Address book\\'s description nullable';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE addressbooks CHANGE description description LONGTEXT DEFAULT NULL');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE addressbooks CHANGE description description LONGTEXT CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20210928132307.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Add default value for calendar order.\n */\nfinal class Version20210928132307 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Add default value for calendar order.';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE calendarinstances CHANGE calendarorder calendarorder INT DEFAULT 0 NOT NULL');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE calendarinstances CHANGE calendarorder calendarorder INT NOT NULL');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20221106220411.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Change VARBINARY to VARCHAR to allow better PostgreSQL support in later migrations.\n */\nfinal class Version20221106220411 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Change VARBINARY to VARCHAR to allow better PostgreSQL support in later migrations';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE addressbookchanges CHANGE uri uri VARCHAR(255) NOT NULL');\n        $this->addSql('ALTER TABLE addressbooks CHANGE principaluri principaluri VARCHAR(255) NOT NULL, CHANGE uri uri VARCHAR(255) NOT NULL');\n        $this->addSql('ALTER TABLE calendarchanges CHANGE uri uri VARCHAR(255) NOT NULL');\n        $this->addSql('ALTER TABLE calendarinstances CHANGE principaluri principaluri VARCHAR(255) DEFAULT NULL, CHANGE uri uri VARCHAR(255) DEFAULT NULL, CHANGE calendarcolor calendarcolor VARCHAR(10) DEFAULT NULL, CHANGE share_href share_href VARCHAR(255) DEFAULT NULL');\n        $this->addSql('ALTER TABLE calendarobjects CHANGE uri uri VARCHAR(255) DEFAULT NULL, CHANGE etag etag VARCHAR(255) DEFAULT NULL, CHANGE componenttype componenttype VARCHAR(255) DEFAULT NULL, CHANGE uid uid VARCHAR(255) DEFAULT NULL');\n        $this->addSql('ALTER TABLE calendars CHANGE components components VARCHAR(255) DEFAULT NULL');\n        $this->addSql('ALTER TABLE calendarsubscriptions CHANGE uri uri VARCHAR(255) NOT NULL, CHANGE principaluri principaluri VARCHAR(255) NOT NULL, CHANGE calendarcolor calendarcolor VARCHAR(10) DEFAULT NULL');\n        $this->addSql('ALTER TABLE cards CHANGE uri uri VARCHAR(255) DEFAULT NULL, CHANGE etag etag VARCHAR(32) DEFAULT NULL');\n        $this->addSql('ALTER TABLE locks CHANGE token token VARCHAR(255) DEFAULT NULL, CHANGE uri uri VARCHAR(255) DEFAULT NULL');\n        $this->addSql('ALTER TABLE principals CHANGE uri uri VARCHAR(255) NOT NULL, CHANGE email email VARCHAR(255) DEFAULT NULL');\n        $this->addSql('CREATE UNIQUE INDEX UNIQ_E797E7FB841CB121 ON principals (uri)');\n        $this->addSql('ALTER TABLE schedulingobjects CHANGE principaluri principaluri VARCHAR(255) DEFAULT NULL, CHANGE uri uri VARCHAR(255) DEFAULT NULL, CHANGE etag etag VARCHAR(255) DEFAULT NULL');\n        $this->addSql('ALTER TABLE users CHANGE digesta1 digesta1 VARCHAR(255) NOT NULL, CHANGE username username VARCHAR(255) NOT NULL');\n        $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9F85E0677 ON users (username)');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE addressbookchanges CHANGE uri uri VARBINARY(255) NOT NULL');\n        $this->addSql('ALTER TABLE addressbooks CHANGE principaluri principaluri VARBINARY(255) NOT NULL, CHANGE uri uri VARBINARY(255) NOT NULL');\n        $this->addSql('ALTER TABLE calendarchanges CHANGE uri uri VARBINARY(255) NOT NULL');\n        $this->addSql('ALTER TABLE calendarinstances CHANGE principaluri principaluri VARBINARY(255) DEFAULT NULL, CHANGE uri uri VARBINARY(255) DEFAULT NULL, CHANGE calendarcolor calendarcolor VARBINARY(10) DEFAULT NULL, CHANGE share_href share_href VARBINARY(255) DEFAULT NULL');\n        $this->addSql('ALTER TABLE calendarobjects CHANGE uri uri VARBINARY(255) DEFAULT NULL, CHANGE etag etag VARBINARY(255) DEFAULT NULL, CHANGE componenttype componenttype VARBINARY(255) DEFAULT NULL, CHANGE uid uid VARBINARY(255) DEFAULT NULL');\n        $this->addSql('ALTER TABLE calendars CHANGE components components VARBINARY(255) DEFAULT NULL');\n        $this->addSql('ALTER TABLE calendarsubscriptions CHANGE uri uri VARBINARY(255) NOT NULL, CHANGE principaluri principaluri VARBINARY(255) NOT NULL, CHANGE calendarcolor calendarcolor VARBINARY(10) DEFAULT NULL');\n        $this->addSql('ALTER TABLE cards CHANGE uri uri VARBINARY(255) DEFAULT NULL, CHANGE etag etag VARBINARY(32) DEFAULT NULL');\n        $this->addSql('ALTER TABLE locks CHANGE token token VARBINARY(255) DEFAULT NULL, CHANGE uri uri VARBINARY(255) DEFAULT NULL');\n        $this->addSql('DROP INDEX UNIQ_E797E7FB841CB121 ON principals');\n        $this->addSql('ALTER TABLE principals CHANGE uri uri VARBINARY(255) NOT NULL, CHANGE email email VARBINARY(255) DEFAULT NULL');\n        $this->addSql('ALTER TABLE schedulingobjects CHANGE principaluri principaluri VARBINARY(255) DEFAULT NULL, CHANGE uri uri VARBINARY(255) DEFAULT NULL, CHANGE etag etag VARBINARY(255) DEFAULT NULL');\n        $this->addSql('DROP INDEX UNIQ_1483A5E9F85E0677 ON users');\n        $this->addSql('ALTER TABLE users CHANGE digesta1 digesta1 VARBINARY(255) NOT NULL, CHANGE username username VARBINARY(255) NOT NULL');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20221106220412.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * PostgreSQL - Initial migration: Create all necessary sabre/dav tables.\n */\nfinal class Version20221106220412 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return '[PostgreSQL] Initial migration: Create all necessary sabre/dav tables';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'postgresql\\'. Skipping it is fine.');\n\n        $this->addSql('CREATE SEQUENCE addressbooks_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE calendars_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE cards_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE calendarsubscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE schedulingobjects_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE locks_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE calendarinstances_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE addressbookchanges_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE principals_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE calendarchanges_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE users_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE calendarobjects_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE SEQUENCE propertystorage_id_seq INCREMENT BY 1 MINVALUE 1 START 1;');\n        $this->addSql('CREATE TABLE addressbooks (id INT NOT NULL, principaluri VARCHAR(255) NOT NULL, displayname VARCHAR(255) NOT NULL, uri VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, synctoken VARCHAR(255) NOT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE TABLE calendars (id INT NOT NULL, synctoken VARCHAR(255) NOT NULL, components VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE TABLE cards (id INT NOT NULL, addressbookid INT NOT NULL, carddata BYTEA DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL, lastmodified INT DEFAULT NULL, etag VARCHAR(32) DEFAULT NULL, size INT NOT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE INDEX IDX_4C258FD8B26C2E9 ON cards (addressbookid);');\n        $this->addSql('CREATE TABLE calendarsubscriptions (id INT NOT NULL, uri VARCHAR(255) NOT NULL, principaluri VARCHAR(255) NOT NULL, source TEXT DEFAULT NULL, displayname VARCHAR(255) DEFAULT NULL, refreshrate VARCHAR(10) DEFAULT NULL, calendarorder INT NOT NULL, calendarcolor VARCHAR(10) DEFAULT NULL, striptodos SMALLINT DEFAULT NULL, stripalarms SMALLINT DEFAULT NULL, stripattachments SMALLINT DEFAULT NULL, lastmodified INT DEFAULT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE TABLE schedulingobjects (id INT NOT NULL, principaluri VARCHAR(255) DEFAULT NULL, calendardata BYTEA DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL, lastmodified INT DEFAULT NULL, etag VARCHAR(255) DEFAULT NULL, size INT NOT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE TABLE locks (id INT NOT NULL, owner VARCHAR(255) DEFAULT NULL, timeout INT DEFAULT NULL, created INT DEFAULT NULL, token VARCHAR(255) DEFAULT NULL, scope SMALLINT DEFAULT NULL, depth SMALLINT DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE TABLE calendarinstances (id INT NOT NULL, calendarid INT NOT NULL, principaluri VARCHAR(255) DEFAULT NULL, access SMALLINT DEFAULT 1 NOT NULL, displayname VARCHAR(255) DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, calendarorder INT DEFAULT 0 NOT NULL, calendarcolor VARCHAR(10) DEFAULT NULL, timezone TEXT DEFAULT NULL, transparent INT DEFAULT NULL, share_href VARCHAR(255) DEFAULT NULL, share_displayname VARCHAR(255) DEFAULT NULL, share_invitestatus INT DEFAULT 2 NOT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE INDEX IDX_51856561B8CB7204 ON calendarinstances (calendarid);');\n        $this->addSql('CREATE TABLE addressbookchanges (id INT NOT NULL, addressbookid INT NOT NULL, uri VARCHAR(255) NOT NULL, synctoken VARCHAR(255) NOT NULL, operation INT NOT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE INDEX IDX_EB122CD58B26C2E9 ON addressbookchanges (addressbookid);');\n        $this->addSql('CREATE TABLE principals (id INT NOT NULL, uri VARCHAR(255) NOT NULL, email VARCHAR(255) DEFAULT NULL, displayname VARCHAR(255) DEFAULT NULL, is_main BOOLEAN NOT NULL, is_admin BOOLEAN NOT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE UNIQUE INDEX UNIQ_E797E7FB841CB121 ON principals (uri);');\n        $this->addSql('CREATE TABLE groupmembers (principal_id INT NOT NULL, member_id INT NOT NULL, PRIMARY KEY(principal_id, member_id));');\n        $this->addSql('CREATE INDEX IDX_6F15EDAC474870EE ON groupmembers (principal_id);');\n        $this->addSql('CREATE INDEX IDX_6F15EDAC7597D3FE ON groupmembers (member_id);');\n        $this->addSql('CREATE TABLE calendarchanges (id INT NOT NULL, calendarid INT NOT NULL, uri VARCHAR(255) NOT NULL, synctoken INT NOT NULL, operation SMALLINT NOT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE INDEX IDX_737547E2B8CB7204 ON calendarchanges (calendarid);');\n        $this->addSql('CREATE TABLE users (id INT NOT NULL, username VARCHAR(255) NOT NULL, digesta1 VARCHAR(255) NOT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9F85E0677 ON users (username);');\n        $this->addSql('CREATE TABLE calendarobjects (id INT NOT NULL, calendarid INT NOT NULL, calendardata BYTEA DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL, lastmodified INT DEFAULT NULL, etag VARCHAR(255) DEFAULT NULL, size INT NOT NULL, componenttype VARCHAR(255) DEFAULT NULL, firstoccurence INT DEFAULT NULL, lastoccurence INT DEFAULT NULL, uid VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id));');\n        $this->addSql('CREATE INDEX IDX_E14F332CB8CB7204 ON calendarobjects (calendarid);');\n        $this->addSql('CREATE TABLE propertystorage (id INT NOT NULL, path VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, valuetype INT DEFAULT NULL, value TEXT DEFAULT NULL, PRIMARY KEY(id));');\n        $this->addSql('ALTER TABLE cards ADD CONSTRAINT FK_4C258FD8B26C2E9 FOREIGN KEY (addressbookid) REFERENCES addressbooks (id) NOT DEFERRABLE INITIALLY IMMEDIATE;');\n        $this->addSql('ALTER TABLE calendarinstances ADD CONSTRAINT FK_51856561B8CB7204 FOREIGN KEY (calendarid) REFERENCES calendars (id) NOT DEFERRABLE INITIALLY IMMEDIATE;');\n        $this->addSql('ALTER TABLE addressbookchanges ADD CONSTRAINT FK_EB122CD58B26C2E9 FOREIGN KEY (addressbookid) REFERENCES addressbooks (id) NOT DEFERRABLE INITIALLY IMMEDIATE;');\n        $this->addSql('ALTER TABLE groupmembers ADD CONSTRAINT FK_6F15EDAC474870EE FOREIGN KEY (principal_id) REFERENCES principals (id) NOT DEFERRABLE INITIALLY IMMEDIATE;');\n        $this->addSql('ALTER TABLE groupmembers ADD CONSTRAINT FK_6F15EDAC7597D3FE FOREIGN KEY (member_id) REFERENCES principals (id) NOT DEFERRABLE INITIALLY IMMEDIATE;');\n        $this->addSql('ALTER TABLE calendarchanges ADD CONSTRAINT FK_737547E2B8CB7204 FOREIGN KEY (calendarid) REFERENCES calendars (id) NOT DEFERRABLE INITIALLY IMMEDIATE;');\n        $this->addSql('ALTER TABLE calendarobjects ADD CONSTRAINT FK_E14F332CB8CB7204 FOREIGN KEY (calendarid) REFERENCES calendars (id) NOT DEFERRABLE INITIALLY IMMEDIATE;');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'postgresql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE cards DROP CONSTRAINT FK_4C258FD8B26C2E9;');\n        $this->addSql('ALTER TABLE calendarinstances DROP CONSTRAINT FK_51856561B8CB7204;');\n        $this->addSql('ALTER TABLE addressbookchanges DROP CONSTRAINT FK_EB122CD58B26C2E9;');\n        $this->addSql('ALTER TABLE groupmembers DROP CONSTRAINT FK_6F15EDAC474870EE;');\n        $this->addSql('ALTER TABLE groupmembers DROP CONSTRAINT FK_6F15EDAC7597D3FE;');\n        $this->addSql('ALTER TABLE calendarchanges DROP CONSTRAINT FK_737547E2B8CB7204;');\n        $this->addSql('ALTER TABLE calendarobjects DROP CONSTRAINT FK_E14F332CB8CB7204;');\n        $this->addSql('DROP SEQUENCE addressbooks_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE calendars_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE cards_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE calendarsubscriptions_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE schedulingobjects_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE locks_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE calendarinstances_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE addressbookchanges_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE principals_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE calendarchanges_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE users_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE calendarobjects_id_seq CASCADE;');\n        $this->addSql('DROP SEQUENCE propertystorage_id_seq CASCADE;');\n        $this->addSql('DROP TABLE addressbooks;');\n        $this->addSql('DROP TABLE calendars;');\n        $this->addSql('DROP TABLE cards;');\n        $this->addSql('DROP TABLE calendarsubscriptions;');\n        $this->addSql('DROP TABLE schedulingobjects;');\n        $this->addSql('DROP TABLE locks;');\n        $this->addSql('DROP TABLE calendarinstances;');\n        $this->addSql('DROP TABLE addressbookchanges;');\n        $this->addSql('DROP TABLE principals;');\n        $this->addSql('DROP TABLE groupmembers;');\n        $this->addSql('DROP TABLE calendarchanges;');\n        $this->addSql('DROP TABLE users;');\n        $this->addSql('DROP TABLE calendarobjects;');\n        $this->addSql('DROP TABLE propertystorage;');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20221211154443.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * SQLite - Initial migration: Create all necessary sabre/dav tables.\n */\nfinal class Version20221211154443 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return '[SQLite] Initial migration: Create all necessary sabre/dav tables';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('sqlite' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'sqlite\\'. Skipping it is fine.');\n\n        $this->addSql('CREATE TABLE addressbookchanges (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, addressbookid INTEGER NOT NULL, uri VARCHAR(255) NOT NULL, synctoken VARCHAR(255) NOT NULL, operation INTEGER NOT NULL)');\n        $this->addSql('CREATE INDEX IDX_EB122CD58B26C2E9 ON addressbookchanges (addressbookid)');\n        $this->addSql('CREATE TABLE addressbooks (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, principaluri VARCHAR(255) NOT NULL, displayname VARCHAR(255) NOT NULL, uri VARCHAR(255) NOT NULL, description CLOB DEFAULT NULL, synctoken VARCHAR(255) NOT NULL)');\n        $this->addSql('CREATE TABLE calendarchanges (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, calendarid INTEGER NOT NULL, uri VARCHAR(255) NOT NULL, synctoken INTEGER NOT NULL, operation SMALLINT NOT NULL)');\n        $this->addSql('CREATE INDEX IDX_737547E2B8CB7204 ON calendarchanges (calendarid)');\n        $this->addSql('CREATE TABLE calendarinstances (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, calendarid INTEGER NOT NULL, principaluri VARCHAR(255) DEFAULT NULL, access SMALLINT DEFAULT 1 NOT NULL, displayname VARCHAR(255) DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL, description CLOB DEFAULT NULL, calendarorder INTEGER DEFAULT 0 NOT NULL, calendarcolor VARCHAR(10) DEFAULT NULL, timezone CLOB DEFAULT NULL, transparent INTEGER DEFAULT NULL, share_href VARCHAR(255) DEFAULT NULL, share_displayname VARCHAR(255) DEFAULT NULL, share_invitestatus INTEGER DEFAULT 2 NOT NULL)');\n        $this->addSql('CREATE INDEX IDX_51856561B8CB7204 ON calendarinstances (calendarid)');\n        $this->addSql('CREATE TABLE calendarobjects (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, calendarid INTEGER NOT NULL, calendardata BLOB DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL, lastmodified INTEGER DEFAULT NULL, etag VARCHAR(255) DEFAULT NULL, size INTEGER NOT NULL, componenttype VARCHAR(255) DEFAULT NULL, firstoccurence INTEGER DEFAULT NULL, lastoccurence INTEGER DEFAULT NULL, uid VARCHAR(255) DEFAULT NULL)');\n        $this->addSql('CREATE INDEX IDX_E14F332CB8CB7204 ON calendarobjects (calendarid)');\n        $this->addSql('CREATE TABLE calendars (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, synctoken VARCHAR(255) NOT NULL, components VARCHAR(255) DEFAULT NULL)');\n        $this->addSql('CREATE TABLE calendarsubscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, uri VARCHAR(255) NOT NULL, principaluri VARCHAR(255) NOT NULL, source CLOB DEFAULT NULL, displayname VARCHAR(255) DEFAULT NULL, refreshrate VARCHAR(10) DEFAULT NULL, calendarorder INTEGER NOT NULL, calendarcolor VARCHAR(10) DEFAULT NULL, striptodos SMALLINT DEFAULT NULL, stripalarms SMALLINT DEFAULT NULL, stripattachments SMALLINT DEFAULT NULL, lastmodified INTEGER DEFAULT NULL)');\n        $this->addSql('CREATE TABLE cards (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, addressbookid INTEGER NOT NULL, carddata BLOB DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL, lastmodified INTEGER DEFAULT NULL, etag VARCHAR(32) DEFAULT NULL, size INTEGER NOT NULL)');\n        $this->addSql('CREATE INDEX IDX_4C258FD8B26C2E9 ON cards (addressbookid)');\n        $this->addSql('CREATE TABLE locks (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, owner VARCHAR(255) DEFAULT NULL, timeout INTEGER DEFAULT NULL, created INTEGER DEFAULT NULL, token VARCHAR(255) DEFAULT NULL, scope SMALLINT DEFAULT NULL, depth SMALLINT DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL)');\n        $this->addSql('CREATE TABLE principals (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, uri VARCHAR(255) NOT NULL, email VARCHAR(255) DEFAULT NULL, displayname VARCHAR(255) DEFAULT NULL, is_main BOOLEAN NOT NULL, is_admin BOOLEAN NOT NULL)');\n        $this->addSql('CREATE UNIQUE INDEX UNIQ_E797E7FB841CB121 ON principals (uri)');\n        $this->addSql('CREATE TABLE groupmembers (principal_id INTEGER NOT NULL, member_id INTEGER NOT NULL, PRIMARY KEY(principal_id, member_id))');\n        $this->addSql('CREATE INDEX IDX_6F15EDAC474870EE ON groupmembers (principal_id)');\n        $this->addSql('CREATE INDEX IDX_6F15EDAC7597D3FE ON groupmembers (member_id)');\n        $this->addSql('CREATE TABLE propertystorage (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, path VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, valuetype INTEGER DEFAULT NULL, value CLOB DEFAULT NULL)');\n        $this->addSql('CREATE TABLE schedulingobjects (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, principaluri VARCHAR(255) DEFAULT NULL, calendardata BLOB DEFAULT NULL, uri VARCHAR(255) DEFAULT NULL, lastmodified INTEGER DEFAULT NULL, etag VARCHAR(255) DEFAULT NULL, size INTEGER NOT NULL)');\n        $this->addSql('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(255) NOT NULL, digesta1 VARCHAR(255) NOT NULL)');\n        $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9F85E0677 ON users (username)');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('sqlite' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'sqlite\\'. Skipping it is fine.');\n\n        $this->addSql('DROP TABLE addressbookchanges');\n        $this->addSql('DROP TABLE addressbooks');\n        $this->addSql('DROP TABLE calendarchanges');\n        $this->addSql('DROP TABLE calendarinstances');\n        $this->addSql('DROP TABLE calendarobjects');\n        $this->addSql('DROP TABLE calendars');\n        $this->addSql('DROP TABLE calendarsubscriptions');\n        $this->addSql('DROP TABLE cards');\n        $this->addSql('DROP TABLE locks');\n        $this->addSql('DROP TABLE principals');\n        $this->addSql('DROP TABLE groupmembers');\n        $this->addSql('DROP TABLE propertystorage');\n        $this->addSql('DROP TABLE schedulingobjects');\n        $this->addSql('DROP TABLE users');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20230209142217.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * PostgreSQL - Add missing defaults to IDs (to use sequences).\n */\nfinal class Version20230209142217 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return '[PostgreSQL] Add missing defaults to IDs (to use sequences)';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'postgresql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE addressbooks ALTER COLUMN id SET DEFAULT nextval(\\'addressbooks_id_seq\\');');\n        $this->addSql('ALTER TABLE calendars ALTER COLUMN id SET DEFAULT nextval(\\'calendars_id_seq\\');');\n        $this->addSql('ALTER TABLE cards ALTER COLUMN id SET DEFAULT nextval(\\'cards_id_seq\\');');\n        $this->addSql('ALTER TABLE calendarsubscriptions ALTER COLUMN id SET DEFAULT nextval(\\'calendarsubscriptions_id_seq\\');');\n        $this->addSql('ALTER TABLE schedulingobjects ALTER COLUMN id SET DEFAULT nextval(\\'schedulingobjects_id_seq\\');');\n        $this->addSql('ALTER TABLE locks ALTER COLUMN id SET DEFAULT nextval(\\'locks_id_seq\\');');\n        $this->addSql('ALTER TABLE calendarinstances ALTER COLUMN id SET DEFAULT nextval(\\'calendarinstances_id_seq\\');');\n        $this->addSql('ALTER TABLE addressbookchanges ALTER COLUMN id SET DEFAULT nextval(\\'addressbookchanges_id_seq\\');');\n        $this->addSql('ALTER TABLE principals ALTER COLUMN id SET DEFAULT nextval(\\'principals_id_seq\\');');\n        $this->addSql('ALTER TABLE calendarchanges ALTER COLUMN id SET DEFAULT nextval(\\'calendarchanges_id_seq\\');');\n        $this->addSql('ALTER TABLE users ALTER COLUMN id SET DEFAULT nextval(\\'users_id_seq\\');');\n        $this->addSql('ALTER TABLE calendarobjects ALTER COLUMN id SET DEFAULT nextval(\\'calendarobjects_id_seq\\');');\n        $this->addSql('ALTER TABLE propertystorage ALTER COLUMN id SET DEFAULT nextval(\\'propertystorage_id_seq\\');');\n        $this->addSql('ALTER TABLE addressbooks ALTER COLUMN synctoken TYPE integer USING synctoken::integer;');\n        $this->addSql('ALTER TABLE calendars ALTER COLUMN synctoken TYPE integer USING synctoken::integer;');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'postgresql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE addressbooks ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE calendars ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE cards ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE calendarsubscriptions ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE schedulingobjects ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE locks ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE calendarinstances ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE addressbookchanges ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE principals ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE calendarchanges ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE users ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE calendarobjects ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE propertystorage ALTER COLUMN id DROP DEFAULT;');\n        $this->addSql('ALTER TABLE addressbooks ALTER COLUMN synctoken TYPE varchar(255);');\n        $this->addSql('ALTER TABLE calendars ALTER COLUMN synctoken TYPE varchar(255);');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20231001214111.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Change BLOB to TEXT (https://github.com/tchapi/davis/issues/110).\n */\nfinal class Version20231001214111 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Change BLOB to TEXT';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE calendarobjects CHANGE calendardata calendardata MEDIUMTEXT DEFAULT NULL');\n        $this->addSql('ALTER TABLE cards CHANGE carddata carddata MEDIUMTEXT DEFAULT NULL');\n        $this->addSql('ALTER TABLE schedulingobjects CHANGE calendardata calendardata MEDIUMTEXT DEFAULT NULL');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE schedulingobjects CHANGE calendardata calendardata LONGBLOB DEFAULT NULL');\n        $this->addSql('ALTER TABLE calendarobjects CHANGE calendardata calendardata LONGBLOB DEFAULT NULL');\n        $this->addSql('ALTER TABLE cards CHANGE carddata carddata LONGBLOB DEFAULT NULL');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20231001214112.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * [PostgreSQL] Change BLOB to TEXT (https://github.com/tchapi/davis/issues/110).\n */\nfinal class Version20231001214112 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return '[PostgreSQL] Change BLOB to TEXT';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'postgresql\\'. Skipping it is fine.');\n\n        $this->addSql(\"ALTER TABLE calendarobjects ALTER COLUMN calendardata TYPE TEXT USING convert_from(calendardata, 'utf8')\");\n        $this->addSql(\"ALTER TABLE cards ALTER COLUMN carddata TYPE TEXT USING convert_from(carddata, 'utf8')\");\n        $this->addSql(\"ALTER TABLE schedulingobjects ALTER COLUMN calendardata TYPE TEXT USING convert_from(calendardata, 'utf8')\");\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'postgresql\\'. Skipping it is fine.');\n\n        $this->addSql(\"ALTER TABLE calendarobjects ALTER COLUMN calendardata TYPE BYTEA DEFAULT NULL USING convert_from(calendardata, 'utf8')\");\n        $this->addSql(\"ALTER TABLE cards ALTER COLUMN carddata TYPE BYTEA DEFAULT NULL USING convert_from(carddata, 'utf8')\");\n        $this->addSql(\"ALTER TABLE schedulingobjects ALTER COLUMN calendardata TYPE BYTEA DEFAULT NULL USING convert_from(calendardata, 'utf8')\");\n    }\n}\n"
  },
  {
    "path": "migrations/Version20231001214113.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * [SQLite] Change BLOB to TEXT (https://github.com/tchapi/davis/issues/110).\n */\nfinal class Version20231001214113 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return '[SQLite] Change BLOB to TEXT';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('sqlite' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'sqlite\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE calendarobjects ADD COLUMN new_calendardata TEXT DEFAULT NULL;');\n        $this->addSql('UPDATE calendarobjects SET new_calendardata = CAST(calendardata as TEXT);');\n        $this->addSql('ALTER TABLE calendarobjects RENAME COLUMN calendardata TO old_calendardata;');\n        $this->addSql('ALTER TABLE calendarobjects RENAME COLUMN new_calendardata TO calendardata;');\n        $this->addSql('ALTER TABLE calendarobjects DROP COLUMN old_calendardata;');\n\n        $this->addSql('ALTER TABLE cards ADD COLUMN new_carddata TEXT DEFAULT NULL;');\n        $this->addSql('UPDATE cards SET new_carddata = CAST(carddata as TEXT);');\n        $this->addSql('ALTER TABLE cards RENAME COLUMN carddata TO old_carddata;');\n        $this->addSql('ALTER TABLE cards RENAME COLUMN new_carddata TO carddata;');\n        $this->addSql('ALTER TABLE cards DROP COLUMN old_carddata;');\n\n        $this->addSql('ALTER TABLE schedulingobjects ADD COLUMN new_calendardata TEXT DEFAULT NULL;');\n        $this->addSql('UPDATE schedulingobjects SET new_calendardata = CAST(calendardata as TEXT);');\n        $this->addSql('ALTER TABLE schedulingobjects RENAME COLUMN calendardata TO old_calendardata;');\n        $this->addSql('ALTER TABLE schedulingobjects RENAME COLUMN new_calendardata TO calendardata;');\n        $this->addSql('ALTER TABLE schedulingobjects DROP COLUMN old_calendardata;');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('sqlite' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'sqlite\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE calendarobjects ADD COLUMN new_calendardata BLOB DEFAULT NULL;');\n        $this->addSql('UPDATE calendarobjects SET new_calendardata = CAST(calendardata as BLOB);');\n        $this->addSql('ALTER TABLE calendarobjects RENAME COLUMN calendardata TO old_calendardata;');\n        $this->addSql('ALTER TABLE calendarobjects RENAME COLUMN new_calendardata TO calendardata;');\n        $this->addSql('ALTER TABLE calendarobjects DROP COLUMN old_calendardata;');\n\n        $this->addSql('ALTER TABLE cards ADD COLUMN new_carddata BLOB DEFAULT NULL;');\n        $this->addSql('UPDATE cards SET new_carddata = CAST(carddata as BLOB);');\n        $this->addSql('ALTER TABLE cards RENAME COLUMN carddata TO old_carddata;');\n        $this->addSql('ALTER TABLE cards RENAME COLUMN new_carddata TO carddata;');\n        $this->addSql('ALTER TABLE cards DROP COLUMN old_carddata;');\n\n        $this->addSql('ALTER TABLE schedulingobjects ADD COLUMN new_calendardata BLOB DEFAULT NULL;');\n        $this->addSql('UPDATE schedulingobjects SET new_calendardata = CAST(calendardata as BLOB);');\n        $this->addSql('ALTER TABLE schedulingobjects RENAME COLUMN calendardata TO old_calendardata;');\n        $this->addSql('ALTER TABLE schedulingobjects RENAME COLUMN new_calendardata TO calendardata;');\n        $this->addSql('ALTER TABLE schedulingobjects DROP COLUMN old_calendardata;');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20231229203515.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Scale up to MEDIUMTEXT for calendar and card data https://github.com/tchapi/davis/pull/111#issuecomment-1872295498\n * ⚠️ This does not fail if the column is already a MEDIUM TEXT, which has allowed us to change a previous migration\n * without touching this one (see https://github.com/tchapi/davis/issues/128).\n */\nfinal class Version20231229203515 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Scale up to MEDIUMTEXT for calendar and card data';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE calendarobjects CHANGE calendardata calendardata MEDIUMTEXT DEFAULT NULL');\n        $this->addSql('ALTER TABLE cards CHANGE carddata carddata MEDIUMTEXT DEFAULT NULL');\n        $this->addSql('ALTER TABLE schedulingobjects CHANGE calendardata calendardata MEDIUMTEXT DEFAULT NULL');\n    }\n\n    public function down(Schema $schema): void\n    {\n        $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration is specific to \\'mysql\\'. Skipping it is fine.');\n\n        $this->addSql('ALTER TABLE schedulingobjects CHANGE calendardata calendardata TEXT DEFAULT NULL');\n        $this->addSql('ALTER TABLE calendarobjects CHANGE calendardata calendardata TEXT DEFAULT NULL');\n        $this->addSql('ALTER TABLE cards CHANGE carddata carddata TEXT DEFAULT NULL');\n    }\n}\n"
  },
  {
    "path": "migrations/Version20250409193948.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n *  Scale timestamps to big int for the Year 2038 problem.\n */\nfinal class Version20250409193948 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Scale timestamps to big int for the Year 2038 problem';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $this->skipIf('sqlite' === $this->connection->getDatabasePlatform()->getName(), 'This migration is not needed on \\'sqlite\\'. Skipping it is fine.');\n\n        // MySQL\n        if ('mysql' === $this->connection->getDatabasePlatform()->getName()) {\n            $this->addSql('ALTER TABLE calendarobjects CHANGE lastmodified lastmodified BIGINT DEFAULT NULL, CHANGE firstoccurence firstoccurence BIGINT DEFAULT NULL, CHANGE lastoccurence lastoccurence BIGINT DEFAULT NULL');\n            $this->addSql('ALTER TABLE calendarsubscriptions CHANGE lastmodified lastmodified BIGINT DEFAULT NULL');\n            $this->addSql('ALTER TABLE locks CHANGE created created BIGINT DEFAULT NULL');\n            $this->addSql('ALTER TABLE schedulingobjects CHANGE lastmodified lastmodified BIGINT DEFAULT NULL');\n        }\n\n        // Posgres\n        if ('postgresql' === $this->connection->getDatabasePlatform()->getName()) {\n            $this->addSql('ALTER TABLE calendarobjects ALTER COLUMN lastmodified TYPE BIGINT');\n            $this->addSql('ALTER TABLE calendarobjects ALTER COLUMN firstoccurence TYPE BIGINT');\n            $this->addSql('ALTER TABLE calendarobjects ALTER COLUMN lastoccurence TYPE BIGINT');\n            $this->addSql('ALTER TABLE calendarsubscriptions ALTER COLUMN lastmodified TYPE BIGINT');\n            $this->addSql('ALTER TABLE locks ALTER COLUMN created TYPE BIGINT');\n            $this->addSql('ALTER TABLE schedulingobjects ALTER COLUMN lastmodified TYPE BIGINT');\n        }\n    }\n\n    public function down(Schema $schema): void\n    {\n        // No need for a down here, it's fine\n    }\n}\n"
  },
  {
    "path": "migrations/Version20250421163214.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Add birthday calendar.\n */\nfinal class Version20250421163214 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Add birthday calendars';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $engine = $this->connection->getDatabasePlatform()->getName();\n\n        if ('mysql' === $engine) {\n            $this->addSql('ALTER TABLE addressbooks ADD included_in_birthday_calendar TINYINT(1) DEFAULT 0');\n        } elseif ('postgresql' === $engine) {\n            $this->addSql('ALTER TABLE addressbooks ADD COLUMN included_in_birthday_calendar BOOLEAN DEFAULT FALSE;');\n        } elseif ('sqlite' === $engine) {\n            $this->addSql('ALTER TABLE addressbooks ADD COLUMN included_in_birthday_calendar INTEGER DEFAULT 0;');\n        }\n    }\n\n    public function down(Schema $schema): void\n    {\n        if ('mysql' === $this->connection->getDatabasePlatform()->getName()) {\n            $this->addSql('ALTER TABLE addressbooks DROP included_in_birthday_calendar');\n        } else {\n            $this->addSql('ALTER TABLE addressbooks DROP COLUMN included_in_birthday_calendar');\n        }\n    }\n}\n"
  },
  {
    "path": "migrations/Version20260131161930.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Add public flag on calendar instance.\n */\nfinal class Version20260131161930 extends AbstractMigration\n{\n    public function getDescription(): string\n    {\n        return 'Add public flag on CalendarInstance (replacing ACCESS_PUBLIC flag)';\n    }\n\n    public function up(Schema $schema): void\n    {\n        $engine = $this->connection->getDatabasePlatform()->getName();\n\n        if ('mysql' === $engine) {\n            $this->addSql('ALTER TABLE calendarinstances ADD public TINYINT(1) DEFAULT 0 NOT NULL');\n        } elseif ('postgresql' === $engine) {\n            $this->addSql('ALTER TABLE calendarinstances ADD public BOOLEAN DEFAULT FALSE NOT NULL');\n        } elseif ('sqlite' === $engine) {\n            $this->addSql('ALTER TABLE calendarinstances ADD public BOOLEAN DEFAULT 0 NOT NULL');\n        }\n\n        // Migrate ACCESS_PUBLIC (10) to ACCESS_SHAREDOWNER (1) + public = true\n        if ('postgresql' === $engine) {\n            $this->addSql('UPDATE calendarinstances SET public = TRUE, access = 1 WHERE access = 10');\n        } else {\n            // MySQL and SQLite accept 1/0 for booleans\n            $this->addSql('UPDATE calendarinstances SET public = 1, access = 1 WHERE access = 10');\n        }\n    }\n\n    public function down(Schema $schema): void\n    {\n        $engine = $this->connection->getDatabasePlatform()->getName();\n\n        // Revert public = true back to ACCESS_PUBLIC (10)\n        if ('postgresql' === $engine) {\n            $this->addSql('UPDATE calendarinstances SET access = 10 WHERE is_public = TRUE');\n        } else {\n            $this->addSql('UPDATE calendarinstances SET access = 10 WHERE is_public = 1');\n        }\n\n        if ('mysql' === $engine) {\n            $this->addSql('ALTER TABLE calendarinstances DROP public');\n        } elseif ('postgresql' === $engine) {\n            $this->addSql('ALTER TABLE calendarinstances DROP COLUMN public');\n        } elseif ('sqlite' === $engine) {\n            $this->addSql('ALTER TABLE calendarinstances DROP COLUMN public');\n        }\n    }\n}\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.5/phpunit.xsd\" backupGlobals=\"false\" colors=\"true\" bootstrap=\"tests/bootstrap.php\" processIsolation=\"true\" cacheResult =\"false\">\n  <php>\n    <ini name=\"display_errors\" value=\"1\"/>\n    <ini name=\"error_reporting\" value=\"-1\"/>\n    <server name=\"APP_ENV\" value=\"test\" force=\"true\"/>\n    <server name=\"SHELL_VERBOSITY\" value=\"-1\"/>\n    <server name=\"SYMFONY_PHPUNIT_REMOVE\" value=\"\"/>\n    <server name=\"SYMFONY_PHPUNIT_VERSION\" value=\"10.5\"/>\n  </php>\n  <testsuites>\n    <testsuite name=\"Project Test Suite\">\n      <directory>tests</directory>\n    </testsuite>\n  </testsuites>\n  <source>\n    <include>\n      <directory suffix=\".php\">src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "public/.htaccess",
    "content": "# Use the front controller as index file. It serves as a fallback solution when\n# every other rewrite/redirect fails (e.g. in an aliased environment without\n# mod_rewrite). Additionally, this reduces the matching process for the\n# start page (path \"/\") because otherwise Apache will apply the rewriting rules\n# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).\nDirectoryIndex index.php\n\n# By default, Apache does not evaluate symbolic links if you did not enable this\n# feature in your server configuration. Uncomment the following line if you\n# install assets as symlinks or if you experience problems related to symlinks\n# when compiling LESS/Sass/CoffeScript assets.\n# Options +FollowSymlinks\n\n# Disabling MultiViews prevents unwanted negotiation, e.g. \"/index\" should not resolve\n# to the front controller \"/index.php\" but be rewritten to \"/index.php/index\".\n<IfModule mod_negotiation.c>\n    Options -MultiViews\n</IfModule>\n\n<IfModule mod_rewrite.c>\n    RewriteEngine On\n\n    # Add .well-known redirections\n    RewriteRule ^\\.well-known/carddav /dav/ [R=301,L]\n    RewriteRule ^\\.well-known/caldav /dav/ [R=301,L]\n\n    # Determine the RewriteBase automatically and set it as environment variable.\n    # If you are using Apache aliases to do mass virtual hosting or installed the\n    # project in a subdirectory, the base path will be prepended to allow proper\n    # resolution of the index.php file and to redirect to the correct URI. It will\n    # work in environments without path prefix as well, providing a safe, one-size\n    # fits all solution. But as you do not need it in this case, you can comment\n    # the following 2 lines to eliminate the overhead.\n    RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\\2$\n    RewriteRule .* - [E=BASE:%1]\n\n    # Sets the HTTP_AUTHORIZATION header removed by Apache\n    RewriteCond %{HTTP:Authorization} .+\n    RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]\n\n    # Redirect to URI without front controller to prevent duplicate content\n    # (with and without `/index.php`). Only do this redirect on the initial\n    # rewrite by Apache and not on subsequent cycles. Otherwise we would get an\n    # endless redirect loop (request -> rewrite to front controller ->\n    # redirect -> request -> ...).\n    # So in case you get a \"too many redirects\" error or you always get redirected\n    # to the start page because your Apache does not expose the REDIRECT_STATUS\n    # environment variable, you have 2 choices:\n    # - disable this feature by commenting the following 2 lines or\n    # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the\n    #   following RewriteCond (best solution)\n    RewriteCond %{ENV:REDIRECT_STATUS} =\"\"\n    RewriteRule ^index\\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]\n\n    # If the requested filename exists, simply serve it.\n    # We only want to let Apache serve files and not directories.\n    # Rewrite all other queries to the front controller.\n    RewriteCond %{REQUEST_FILENAME} !-f\n    RewriteRule ^ %{ENV:BASE}/index.php [L]\n</IfModule>\n\n<IfModule !mod_rewrite.c>\n    <IfModule mod_alias.c>\n        # When mod_rewrite is not available, we instruct a temporary redirect of\n        # the start page to the front controller explicitly so that the website\n        # and the generated links can still be used.\n        RedirectMatch 307 ^/$ /index.php/\n        # RedirectTemp cannot be used instead\n    </IfModule>\n</IfModule>\n"
  },
  {
    "path": "public/css/style.css",
    "content": "body {\n    padding-top: calc(56px + 30px);\n    -webkit-touch-callout: none; /* iOS Safari */\n    -webkit-user-select: none; /* Safari */\n    -khtml-user-select: none; /* Konqueror HTML */\n    -moz-user-select: none; /* Firefox */\n    -ms-user-select: none; /* Internet Explorer/Edge */\n    user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */\n}\n\n/* Add a simple line below display headings */\n.display-4 {\n    font-size: 2.5rem;\n    border-bottom: 1px solid var(--bs-border-color);\n}\n\n/* Flashes (messages) styles */\n.flashes {\n    position: fixed;\n    top: 20px;\n    right: 0;\n    z-index: 1050;\n}\n.flashes .inner {\n    position: absolute;\n    top: 0;\n    right: 20px;\n}\n.flashes .toast {\n    min-width: 270px;\n}\n\n/* Little color swatch to show calendar color */\n#calendar_instance_calendarColor_help {\n position: relative;\n}\n#calendar_instance_calendarColor_help::after {\n    content: \"\";\n    width: 30px;\n    height: 30px;\n    position: absolute;\n    top: -38px;\n    right: 5px;\n    background: var(--calendar-color);\n    border-radius: 3px;\n    border: 1px solid #AAA;\n}\n\n/* Special indicator badge */\n.badge.badge-indicator {\n    height: 15px;\n    width: 15px;\n    padding: 0;\n    border-radius: 50%;\n    text-decoration: none;\n    color: var(--bs-heading-color); /* Comes from Bootstrap */\n}\n\n/* Allow selection in the Bootstrap popover */\n.popover .popover-body {\n    user-select: text;\n}\n\n/* Github link icon */\n.github-link {\n    background-image: url(\"/images/github-mark.png\");\n    background-repeat: no-repeat;\n    background-size: 23px;\n    height: 23px;\n    width: 23px;\n}\n[data-bs-theme=dark] .github-link {\n    background-image: url(\"/images/github-mark-white.png\");\n}\n"
  },
  {
    "path": "public/index.php",
    "content": "<?php\n\n$overridenEnvDir = getenv('ENV_DIR') ?: null;\n\nif ($overridenEnvDir) {\n    // Tell the Runtime not to touch dotenv so we can load our own file.\n    // This is needed if the ENV_DIR is outside of the project directory\n    $_SERVER['APP_RUNTIME_OPTIONS']['disable_dotenv'] = true;\n}\n\nuse App\\Kernel;\nuse Symfony\\Component\\Dotenv\\Dotenv;\n\nrequire_once dirname(__DIR__).'/vendor/autoload_runtime.php';\n\nif ($overridenEnvDir) {\n    // Load our own now, after the runtime has booted\n    (new Dotenv())->bootEnv($overridenEnvDir.'/.env');\n}\n\nreturn function (array $context) {\n    return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);\n};\n"
  },
  {
    "path": "public/js/app.js",
    "content": "'use strict'\n\n// Calendar share modal\nconst shareModal = document.getElementById('shareModal')\nif (shareModal) {\n    shareModal.addEventListener('show.bs.modal', event => {\n        // Button that triggered the modal\n        const button = event.relatedTarget\n\n        // Grab calendar shares url and add url\n        let shareesUrl = button.getAttribute('data-sharees-href');\n        let targetUrl = button.getAttribute('data-href');\n\n        // When adding the sharee, catch the click to add the query parameter\n        const addShareeButton = document.getElementById('shareModal-addSharee');\n        addShareeButton.addEventListener(\"click\", function(e) {\n            const writeAccess = document.getElementById('shareModal-writeAccess').checked ? 'true' : 'false';\n            const principalId = document.getElementById('shareModal-member').value;\n\n            e.preventDefault()\n            window.location = targetUrl + \"?principalId=\" + principalId + \"&write=\" + writeAccess\n        });\n\n        const noneElement = document.getElementById('shareModal-none')\n\n        // Shares list\n        const shares = document.getElementById('shareModal-shares')\n        shares.innerHTML = ''\n\n        // Get calendar shares\n        fetch(shareesUrl)\n            .then((response) => response.json())\n            .then((data) => {\n\n                // No sharee\n                if (data.length === 0) {\n                    noneElement.classList.remove(\"d-none\");\n                    return\n                }\n    \n                noneElement.classList.add('d-none')\n\n                // Share list item template\n                const template = document.getElementById(\"shareModal-shareeTemplate\");\n\n                data.forEach(element => {\n                    const clone = template.content.cloneNode(true);\n                    let name = clone.querySelectorAll(\"span.name\");\n                    name[0].textContent = element.displayName;\n                    let badge = clone.querySelectorAll(\"span.badge\");\n                    badge[0].textContent = element.accessText;\n                    if (element.isWriteAccess) {\n                        badge[0].classList.add('bg-success')\n                        badge[0].classList.remove('bg-info')\n                    }\n                    let revokeButton = clone.querySelectorAll(\"a.revoke\");\n                    revokeButton[0].href = element.revokeUrl;\n\n                    shares.appendChild(clone);\n                });\n                \n            });\n    })\n}\n\n\n// Delete modals (all kind of entities, so we use the rel, not the id)\nconst deleteModals = document.querySelectorAll('[rel=\"deleteModal\"]');\ndeleteModals.forEach(element => {\n    element.addEventListener('show.bs.modal', event => {\n        // Button that triggered the modal\n        const button = event.relatedTarget\n\n        // Grab real target url for deletion\n        let targetUrl = button.getAttribute('data-href');\n        let modalFlavour = button.getAttribute('data-flavour');\n\n        // Put it into the modal's OK button\n        const deleteCTA = document.getElementById(`deleteModal-${modalFlavour}-cta`);\n        console.log(\"setting href to \" + targetUrl)\n        deleteCTA.setAttribute('href', targetUrl);\n    })\n})\n\n\n\n// Global account delegation modal\nconst addDelegateModal = document.getElementById('addDelegateModal')\nif (addDelegateModal) {\n    addDelegateModal.addEventListener('show.bs.modal', event => {\n        // When adding the sharee, catch the click to add the query parameter\n        const addDelegateButton = document.getElementById('addDelegateModal-cta');\n        addDelegateButton.addEventListener(\"click\", function(e) {\n            const targetUrl = addDelegateButton.getAttribute('data-href');\n            const writeAccess = document.getElementById('addDelegateModal-writeAccess').checked ? 'true' : 'false';\n            const principalId = document.getElementById('addDelegateModal-member').value;\n\n            e.preventDefault()\n            window.location = targetUrl + \"?principalId=\" + principalId + \"&write=\" + writeAccess\n        });\n\n    })\n}\n\n// Color swatch: update it live (not working in IE ¯\\_(ツ)_/¯ but it's just a nice to have)\nconst colorPicker = document.getElementById('calendar_instance_calendarColor');\nif (colorPicker) {\n    colorPicker.addEventListener('keyup', event => {\n        document.body.style.setProperty('--calendar-color', event.target.value);\n    })\n    document.body.style.setProperty('--calendar-color', colorPicker.value);\n}\n\n// Bootstrap 5 popovers\nconst popoverTriggerList = document.querySelectorAll('[data-bs-toggle=\"popover\"]')\nif (popoverTriggerList) {\n    [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))\n}\n\n// Bootstrap 5 toasts\nconst toastElList = document.querySelectorAll('.toast')\nif (toastElList) {\n    [...toastElList].map(toastEl => {\n        const toast = new bootstrap.Toast(toastEl)\n        toast.show()\n    })\n}\n"
  },
  {
    "path": "public/js/color.mode.toggler.js",
    "content": "/* Based on Bootstrap' color mode toggler */\n/*!\n * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)\n * Copyright 2011-2023 The Bootstrap Authors\n * Licensed under the Creative Commons Attribution 3.0 Unported License.\n */\n\n(() => {\n    'use strict'\n  \n    const getStoredTheme = () => localStorage.getItem('theme')\n    const setStoredTheme = theme => localStorage.setItem('theme', theme)\n  \n    const getPreferredTheme = () => {\n      const storedTheme = getStoredTheme()\n      if (storedTheme) {\n        return storedTheme\n      }\n  \n      return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n    }\n  \n    const setTheme = theme => {\n      if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n        document.documentElement.setAttribute('data-bs-theme', 'dark')\n      } else {\n        document.documentElement.setAttribute('data-bs-theme', theme)\n      }\n    }\n  \n    setTheme(getPreferredTheme())\n  \n    const showActiveTheme = (theme, focus = false) => {\n      const themeSwitcher = document.querySelector('#bd-theme')\n  \n      if (!themeSwitcher) {\n        return\n      }\n  \n      const themeSwitcherText = document.querySelector('#bd-theme-text')\n      const activeThemeIcon = document.querySelector('.theme-icon-active')\n      const btnToActive = document.querySelector(`[data-bs-theme-value=\"${theme}\"] .theme-icon`)\n  \n      document.querySelectorAll('[data-bs-theme-value]').forEach(element => {\n        element.classList.remove('active')\n        element.setAttribute('aria-pressed', 'false')\n      })\n  \n      btnToActive.classList.add('active')\n      btnToActive.setAttribute('aria-pressed', 'true')\n      activeThemeIcon.innerHTML = btnToActive.innerHTML\n      const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`\n      themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)\n  \n      if (focus) {\n        themeSwitcher.focus()\n      }\n    }\n  \n    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {\n      const storedTheme = getStoredTheme()\n      if (storedTheme !== 'light' && storedTheme !== 'dark') {\n        setTheme(getPreferredTheme())\n      }\n    })\n  \n    window.addEventListener('DOMContentLoaded', () => {\n      showActiveTheme(getPreferredTheme())\n  \n      document.querySelectorAll('[data-bs-theme-value]')\n        .forEach(toggle => {\n          toggle.addEventListener('click', () => {\n            const theme = toggle.getAttribute('data-bs-theme-value')\n            setStoredTheme(theme)\n            setTheme(theme)\n            showActiveTheme(theme, true)\n          })\n        })\n    })\n  })()"
  },
  {
    "path": "public/robots.txt",
    "content": "User-agent: *\nDisallow: /\n"
  },
  {
    "path": "public/site.webmanifest",
    "content": "{\"name\":\"\",\"short_name\":\"\",\"icons\":[{\"src\":\"/android-chrome-192x192.png\",\"sizes\":\"192x192\",\"type\":\"image/png\"},{\"src\":\"/android-chrome-512x512.png\",\"sizes\":\"512x512\",\"type\":\"image/png\"}],\"theme_color\":\"#ffffff\",\"background_color\":\"#ffffff\",\"display\":\"standalone\"}"
  },
  {
    "path": "src/Command/ApiGenerateCommand.php",
    "content": "<?php\n\nnamespace App\\Command;\n\nuse Symfony\\Component\\Console\\Attribute\\AsCommand;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\n\n#[AsCommand(\n    name: 'api:generate',\n    description: 'Generate a new API key',\n    help: 'This command allows you to generate a new API key',\n)]\nclass ApiGenerateCommand extends Command\n{\n    public function __construct()\n    {\n        parent::__construct();\n    }\n\n    protected function configure(): void\n    {\n        $this\n            ->setName('api:generate')\n            ->setDescription('Generate a new API key')\n            ->setHelp('This command allows you to generate a new API key')\n        ;\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $io = new SymfonyStyle($input, $output);\n        $apiKey = bin2hex(random_bytes(32));\n\n        $io->success($apiKey);\n        $io->warning('Set the API key in your .env file as API_KEY, as it won\\'t be stored otherwise.');\n\n        return self::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "src/Command/SyncBirthdayCalendars.php",
    "content": "<?php\n\nnamespace App\\Command;\n\nuse App\\Entity\\User;\nuse App\\Services\\BirthdayService;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Sabre\\CalDAV\\Backend\\PDO as CalendarBackend;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Helper\\ProgressBar;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass SyncBirthdayCalendars extends Command\n{\n    public function __construct(\n        private ManagerRegistry $doctrine,\n        private BirthdayService $birthdayService,\n    ) {\n        parent::__construct();\n\n        $em = $doctrine->getManager();\n        $pdo = $em->getConnection()->getNativeConnection();\n        $this->birthdayService->setBackend(new CalendarBackend($pdo));\n    }\n\n    protected function configure(): void\n    {\n        $this\n            ->setName('dav:sync-birthday-calendar')\n            ->setDescription('Synchronizes the birthday calendar')\n            ->addArgument('username',\n                InputArgument::OPTIONAL,\n                'Username for whom the birthday calendar will be synchronized');\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $username = $input->getArgument('username');\n\n        if (!is_null($username)) {\n            if (!$this->doctrine->getRepository(User::class)->findOneByUsername($username)) {\n                throw new \\InvalidArgumentException(\"User <$username> is unknown.\");\n            }\n\n            $output->writeln(\"Start birthday calendar sync for $username\");\n            $this->birthdayService->syncUser($username);\n\n            return self::SUCCESS;\n        }\n\n        $output->writeln('Start birthday calendar sync for all users ...');\n        $p = new ProgressBar($output);\n        $p->start();\n\n        $users = $this->doctrine->getRepository(User::class)->findAll();\n\n        foreach ($users as $user) {\n            $p->advance();\n            $this->birthdayService->syncUser($user->getUsername());\n        }\n\n        $p->finish();\n        $output->writeln('');\n\n        return self::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "src/Constants.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App;\n\nclass Constants\n{\n    public const BIRTHDAY_CALENDAR_URI = 'birthday-calendar';\n\n    public const MAX_DATE = '2222-01-01';\n}\n"
  },
  {
    "path": "src/Controller/Admin/AddressBookController.php",
    "content": "<?php\n\nnamespace App\\Controller\\Admin;\n\nuse App\\Entity\\AddressBook;\nuse App\\Entity\\Principal;\nuse App\\Entity\\User;\nuse App\\Form\\AddressBookType;\nuse App\\Services\\BirthdayService;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Contracts\\Translation\\TranslatorInterface;\n\n#[Route('/addressbooks', name: 'addressbook_')]\nclass AddressBookController extends AbstractController\n{\n    #[Route('/{userId}', name: 'index')]\n    public function addressBooks(ManagerRegistry $doctrine, int $userId): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $username = $user->getUsername();\n        $principalUri = Principal::PREFIX.$username;\n\n        $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri);\n        $addressbooks = $doctrine->getRepository(AddressBook::class)->findByPrincipalUri($principalUri);\n\n        return $this->render('addressbooks/index.html.twig', [\n            'addressbooks' => $addressbooks,\n            'principal' => $principal,\n            'userId' => $userId,\n        ]);\n    }\n\n    #[Route('/{userId}/new', name: 'create')]\n    #[Route('/{userId}/edit/{id}', name: 'edit', requirements: ['id' => \"\\d+\"])]\n    public function addressbookCreate(ManagerRegistry $doctrine, Request $request, int $userId, ?int $id, TranslatorInterface $trans, BirthdayService $birthdayService): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $username = $user->getUsername();\n        $principalUri = Principal::PREFIX.$username;\n\n        $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri);\n\n        if (!$principal) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        if ($id) {\n            $addressbook = $doctrine->getRepository(AddressBook::class)->findOneById($id);\n            if (!$addressbook) {\n                throw $this->createNotFoundException('Address book not found');\n            }\n        } else {\n            $addressbook = new AddressBook();\n        }\n\n        $isBirthdayCalendarEnabled = $this->getParameter('caldav_enabled') && $this->getParameter('carddav_enabled');\n\n        $form = $this->createForm(AddressBookType::class, $addressbook, ['new' => !$id, 'birthday_calendar_enabled' => $isBirthdayCalendarEnabled]);\n\n        if ($isBirthdayCalendarEnabled) {\n            $form->get('includedInBirthdayCalendar')->setData($addressbook->isIncludedInBirthdayCalendar());\n        }\n        $form->get('principalUri')->setData($principalUri);\n\n        $form->handleRequest($request);\n\n        if ($form->isSubmitted() && $form->isValid()) {\n            $entityManager = $doctrine->getManager();\n\n            $entityManager->persist($addressbook);\n            $entityManager->flush();\n\n            $this->addFlash('success', $trans->trans('addressbooks.saved'));\n\n            if ($isBirthdayCalendarEnabled && true === $form->get('includedInBirthdayCalendar')->getData()) {\n                $addressbook->setIncludedInBirthdayCalendar(true);\n            } else {\n                $addressbook->setIncludedInBirthdayCalendar(false);\n            }\n\n            if ($isBirthdayCalendarEnabled) {\n                // Let's sync the user birthday calendar if needed\n                $birthdayService->syncUser($username);\n            }\n\n            return $this->redirectToRoute('addressbook_index', ['userId' => $userId]);\n        }\n\n        return $this->render('addressbooks/edit.html.twig', [\n            'form' => $form->createView(),\n            'principal' => $principal,\n            'userId' => $userId,\n            'addressbook' => $addressbook,\n        ]);\n    }\n\n    #[Route('/{userId}/delete/{id}', name: 'delete', requirements: ['id' => \"\\d+\"])]\n    public function addressbookDelete(ManagerRegistry $doctrine, int $userId, string $id, TranslatorInterface $trans, BirthdayService $birthdayService): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $addressbook = $doctrine->getRepository(AddressBook::class)->findOneById($id);\n        if (!$addressbook) {\n            throw $this->createNotFoundException('Address Book not found');\n        }\n\n        $entityManager = $doctrine->getManager();\n\n        foreach ($addressbook->getCards() ?? [] as $card) {\n            $entityManager->remove($card);\n        }\n        foreach ($addressbook->getChanges() ?? [] as $change) {\n            $entityManager->remove($change);\n        }\n        $entityManager->remove($addressbook);\n\n        $entityManager->flush();\n        $this->addFlash('success', $trans->trans('addressbooks.deleted'));\n\n        $isBirthdayCalendarEnabled = $this->getParameter('caldav_enabled') && $this->getParameter('carddav_enabled');\n        if ($isBirthdayCalendarEnabled) {\n            // Let's sync the user birthday calendar if needed\n            $birthdayService->syncUser($user->getUsername());\n        }\n\n        return $this->redirectToRoute('addressbook_index', ['userId' => $userId]);\n    }\n}\n"
  },
  {
    "path": "src/Controller/Admin/CalendarController.php",
    "content": "<?php\n\nnamespace App\\Controller\\Admin;\n\nuse App\\Entity\\Calendar;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\CalendarSubscription;\nuse App\\Entity\\Principal;\nuse App\\Entity\\User;\nuse App\\Form\\CalendarInstanceType;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Sabre\\DAV\\Sharing\\Plugin as SharingPlugin;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\HttpKernel\\Exception\\BadRequestHttpException;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\nuse Symfony\\Contracts\\Translation\\TranslatorInterface;\n\n#[Route('/calendars', name: 'calendar_')]\nclass CalendarController extends AbstractController\n{\n    #[Route('/{userId}', name: 'index')]\n    public function calendars(ManagerRegistry $doctrine, UrlGeneratorInterface $router, int $userId): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $username = $user->getUsername();\n        $principalUri = Principal::PREFIX.$username;\n\n        $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri);\n        $allCalendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUri($principalUri);\n\n        $subscriptions = $doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($principalUri);\n\n        // Separate shared calendars\n        $calendars = [];\n        $shared = [];\n        $auto = [];\n        foreach ($allCalendars as $calendar) {\n            if ($calendar->isAutomaticallyGenerated()) {\n                $auto[] = [\n                    'entity' => $calendar,\n                    'uri' => $router->generate('dav', ['path' => 'calendars/'.$username.'/'.$calendar->getUri()], UrlGeneratorInterface::ABSOLUTE_URL),\n                ];\n            } elseif (!$calendar->isShared()) {\n                $calendars[] = [\n                    'entity' => $calendar,\n                    'uri' => $router->generate('dav', ['path' => 'calendars/'.$username.'/'.$calendar->getUri()], UrlGeneratorInterface::ABSOLUTE_URL),\n                ];\n            } else {\n                $shared[] = [\n                    'entity' => $calendar,\n                    'uri' => $router->generate('dav', ['path' => 'calendars/'.$username.'/'.$calendar->getUri()], UrlGeneratorInterface::ABSOLUTE_URL),\n                ];\n            }\n        }\n\n        // We need all the other users so we can propose to share calendars with them\n        $allPrincipalsExcept = $doctrine->getRepository(Principal::class)->findAllExceptPrincipal($principalUri);\n\n        return $this->render('calendars/index.html.twig', [\n            'calendars' => $calendars,\n            'subscriptions' => $subscriptions,\n            'shared' => $shared,\n            'auto' => $auto,\n            'principal' => $principal,\n            'userId' => $userId,\n            'allPrincipals' => $allPrincipalsExcept,\n        ]);\n    }\n\n    #[Route('/{userId}/new', name: 'create')]\n    #[Route('/{userId}/edit/{id}', name: 'edit', requirements: ['id' => \"\\d+\"])]\n    public function calendarEdit(ManagerRegistry $doctrine, Request $request, int $userId, ?int $id, TranslatorInterface $trans): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $username = $user->getUsername();\n        $principalUri = Principal::PREFIX.$username;\n\n        $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri);\n\n        if (!$principal) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        if ($id) {\n            $calendarInstance = $doctrine->getRepository(CalendarInstance::class)->findOneById($id);\n            if (!$calendarInstance) {\n                throw $this->createNotFoundException('Calendar not found');\n            }\n        } else {\n            $calendarInstance = new CalendarInstance();\n            $calendar = new Calendar();\n            $calendarInstance->setCalendar($calendar);\n        }\n\n        $arePublicCalendarsEnabled = $this->getParameter('public_calendars_enabled');\n\n        $form = $this->createForm(CalendarInstanceType::class, $calendarInstance, [\n            'new' => !$id,\n            'shared' => $calendarInstance->isShared(),\n            'public_calendars_enabled' => $arePublicCalendarsEnabled,\n        ]);\n\n        $components = explode(',', $calendarInstance->getCalendar()->getComponents());\n\n        $form->get('events')->setData(in_array(Calendar::COMPONENT_EVENTS, $components));\n        $form->get('todos')->setData(in_array(Calendar::COMPONENT_TODOS, $components));\n        $form->get('notes')->setData(in_array(Calendar::COMPONENT_NOTES, $components));\n        $form->get('principalUri')->setData($principalUri);\n\n        $form->handleRequest($request);\n\n        $entityManager = $doctrine->getManager();\n\n        if ($form->isSubmitted() && $form->isValid()) {\n            // Only owners can change those\n            if (!$calendarInstance->isShared()) {\n                $components = [];\n                if ($form->get('events')->getData()) {\n                    $components[] = Calendar::COMPONENT_EVENTS;\n                }\n                if ($form->get('todos')->getData()) {\n                    $components[] = Calendar::COMPONENT_TODOS;\n                }\n                if ($form->get('notes')->getData()) {\n                    $components[] = Calendar::COMPONENT_NOTES;\n                }\n                if ($arePublicCalendarsEnabled && true === $form->get('public')->getData()) {\n                    $calendarInstance->setPublic(true);\n                } else {\n                    $calendarInstance->setPublic(false);\n                }\n\n                $calendarInstance->getCalendar()->setComponents(implode(',', $components));\n            }\n\n            // We want to remove all shares if a calendar goes public\n            if ($arePublicCalendarsEnabled && true === $form->get('public')->getData() && $id) {\n                $calendarId = $calendarInstance->getCalendar()->getId();\n                $instances = $doctrine->getRepository(CalendarInstance::class)->findSharedInstancesOfInstance($calendarId, false);\n                foreach ($instances as $instance) {\n                    $entityManager->remove($instance);\n                }\n            }\n\n            $entityManager->persist($calendarInstance);\n            $entityManager->flush();\n\n            $this->addFlash('success', $trans->trans('calendar.saved'));\n\n            return $this->redirectToRoute('calendar_index', ['userId' => $userId]);\n        }\n\n        return $this->render('calendars/edit.html.twig', [\n            'form' => $form->createView(),\n            'principal' => $principal,\n            'userId' => $userId,\n            'calendar' => $calendarInstance,\n        ]);\n    }\n\n    #[Route('/{userId}/shares/{calendarid}', name: 'shares', requirements: ['calendarid' => \"\\d+\"])]\n    public function calendarShares(ManagerRegistry $doctrine, int $userId, string $calendarid, TranslatorInterface $trans): Response\n    {\n        $instances = $doctrine->getRepository(CalendarInstance::class)->findSharedInstancesOfInstance($calendarid, true);\n\n        $response = [];\n        foreach ($instances as $instance) {\n            $response[] = [\n                'principalUri' => $instance[0]['principalUri'],\n                'displayName' => $instance['displayName'],\n                'email' => $instance['email'],\n                'accessText' => $trans->trans('calendar.share_access.'.$instance[0]['access']),\n                'isWriteAccess' => SharingPlugin::ACCESS_READWRITE === $instance[0]['access'],\n                'revokeUrl' => $this->generateUrl('calendar_revoke', ['userId' => $userId, 'id' => $instance[0]['id']]),\n            ];\n        }\n\n        return new JsonResponse($response);\n    }\n\n    #[Route('/{userId}/share/{instanceid}', name: 'share_add', requirements: ['instanceid' => \"\\d+\"])]\n    public function calendarShareAdd(ManagerRegistry $doctrine, Request $request, int $userId, string $instanceid, TranslatorInterface $trans): Response\n    {\n        $instance = $doctrine->getRepository(CalendarInstance::class)->findOneById($instanceid);\n        if (!$instance) {\n            throw $this->createNotFoundException('Calendar not found');\n        }\n\n        if (!is_numeric($request->get('principalId'))) {\n            throw new BadRequestHttpException();\n        }\n\n        $newShareeToAdd = $doctrine->getRepository(Principal::class)->findOneById($request->get('principalId'));\n        if (!$newShareeToAdd) {\n            throw $this->createNotFoundException('Member not found');\n        }\n\n        // Let's check that there wasn't another instance\n        // already existing first, so we can update it:\n        $existingSharedInstance = $doctrine->getRepository(CalendarInstance::class)->findSharedInstanceOfInstanceFor($instance->getCalendar()->getId(), $newShareeToAdd->getUri());\n\n        $writeAccess = ('true' === $request->get('write') ? SharingPlugin::ACCESS_READWRITE : SharingPlugin::ACCESS_READ);\n\n        $entityManager = $doctrine->getManager();\n\n        if ($existingSharedInstance) {\n            $existingSharedInstance->setAccess($writeAccess);\n        } else {\n            $sharedInstance = new CalendarInstance();\n            $sharedInstance->setTransparent(1)\n                     ->setCalendar($instance->getCalendar())\n                     ->setShareHref('mailto:'.$newShareeToAdd->getEmail())\n                     ->setDescription($instance->getDescription())\n                     ->setDisplayName($instance->getDisplayName())\n                     ->setUri(\\Sabre\\DAV\\UUIDUtil::getUUID())\n                     ->setPrincipalUri($newShareeToAdd->getUri())\n                     ->setAccess($writeAccess);\n            $entityManager->persist($sharedInstance);\n        }\n\n        $entityManager->flush();\n        $this->addFlash('success', $trans->trans('calendar.shared'));\n\n        return $this->redirectToRoute('calendar_index', ['userId' => $userId]);\n    }\n\n    #[Route('/{userId}/delete/{id}', name: 'delete', requirements: ['id' => \"\\d+\"])]\n    public function calendarDelete(ManagerRegistry $doctrine, int $userId, string $id, TranslatorInterface $trans): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        $instance = $doctrine->getRepository(CalendarInstance::class)->findOneById($id);\n        if (!$instance) {\n            throw $this->createNotFoundException('Calendar not found');\n        }\n\n        $entityManager = $doctrine->getManager();\n\n        // Scheduling objects attached to the calendar objects of the calendar\n        $schedulingObjectsOfCalendarObjects = $doctrine->getRepository(CalendarInstance::class)->findAllSchedulingObjectsForCalendar($instance->getId(), $principalUri);\n        foreach ($schedulingObjectsOfCalendarObjects ?? [] as $object) {\n            $entityManager->remove($object);\n        }\n        foreach ($instance->getCalendar()->getObjects() ?? [] as $object) {\n            $entityManager->remove($object);\n        }\n        foreach ($instance->getCalendar()->getChanges() ?? [] as $change) {\n            $entityManager->remove($change);\n        }\n\n        // Remove the original calendar instance\n        $entityManager->remove($instance);\n\n        // Remove shared instances\n        $sharedInstances = $doctrine->getRepository(CalendarInstance::class)->findSharedInstancesOfInstance($instance->getCalendar()->getId(), false);\n        foreach ($sharedInstances as $sharedInstance) {\n            $entityManager->remove($sharedInstance);\n        }\n\n        // Finally remove the calendar itself\n        $entityManager->remove($instance->getCalendar());\n\n        $entityManager->flush();\n        $this->addFlash('success', $trans->trans('calendar.deleted'));\n\n        return $this->redirectToRoute('calendar_index', ['userId' => $userId]);\n    }\n\n    #[Route('/{userId}/revoke/{id}', name: 'revoke', requirements: ['id' => \"\\d+\"])]\n    public function calendarRevoke(ManagerRegistry $doctrine, int $userId, string $id, TranslatorInterface $trans): Response\n    {\n        $instance = $doctrine->getRepository(CalendarInstance::class)->findOneById($id);\n        if (!$instance) {\n            throw $this->createNotFoundException('Calendar not found');\n        }\n\n        $entityManager = $doctrine->getManager();\n        $entityManager->remove($instance);\n\n        $entityManager->flush();\n        $this->addFlash('success', $trans->trans('calendar.revoked'));\n\n        return $this->redirectToRoute('calendar_index', ['userId' => $userId]);\n    }\n}\n"
  },
  {
    "path": "src/Controller/Admin/DashboardController.php",
    "content": "<?php\n\nnamespace App\\Controller\\Admin;\n\nuse App\\Entity\\AddressBook;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\CalendarObject;\nuse App\\Entity\\Card;\nuse App\\Entity\\User;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\n\nclass DashboardController extends AbstractController\n{\n    #[Route('/dashboard', name: 'dashboard')]\n    public function dashboard(ManagerRegistry $doctrine): Response\n    {\n        $users = $doctrine->getRepository(User::class)->findAll();\n        $calendars = $doctrine->getRepository(CalendarInstance::class)->findAll();\n        $addressbooks = $doctrine->getRepository(AddressBook::class)->findAll();\n        $events = $doctrine->getRepository(CalendarObject::class)->findAll();\n        $contacts = $doctrine->getRepository(Card::class)->findAll();\n\n        $timezoneParameter = $this->getParameter('timezone');\n\n        return $this->render('dashboard.html.twig', [\n            'users' => $users,\n            'calendars' => $calendars,\n            'addressbooks' => $addressbooks,\n            'events' => $events,\n            'contacts' => $contacts,\n            'timezone' => [\n                'actual_default' => date_default_timezone_get(),\n                'not_set_in_app' => '' === $timezoneParameter,\n                'bad_value' => '' !== $timezoneParameter && !in_array($timezoneParameter, \\DateTimeZone::listIdentifiers()),\n            ],\n            'version' => \\App\\Version::VERSION,\n            'sabredav_version' => \\Sabre\\DAV\\Version::VERSION,\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/Controller/Admin/UserController.php",
    "content": "<?php\n\nnamespace App\\Controller\\Admin;\n\nuse App\\Entity\\AddressBook;\nuse App\\Entity\\Calendar;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\CalendarSubscription;\nuse App\\Entity\\Principal;\nuse App\\Entity\\SchedulingObject;\nuse App\\Entity\\User;\nuse App\\Form\\UserType;\nuse App\\Services\\Utils;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\HttpKernel\\Exception\\BadRequestHttpException;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Contracts\\Translation\\TranslatorInterface;\n\n#[Route('/users', name: 'user_')]\nclass UserController extends AbstractController\n{\n    #[Route('/', name: 'index')]\n    public function users(ManagerRegistry $doctrine): Response\n    {\n        $results = $doctrine->getRepository(Principal::class)->findAllMainPrincipalsWithUserIds();\n\n        return $this->render('users/index.html.twig', [\n            'results' => $results,\n        ]);\n    }\n\n    #[Route('/new', name: 'create')]\n    #[Route('/edit/{userId}', name: 'edit')]\n    public function userCreate(ManagerRegistry $doctrine, Utils $utils, Request $request, ?int $userId, TranslatorInterface $trans): Response\n    {\n        if ($userId) {\n            $user = $doctrine->getRepository(User::class)->findOneById($userId);\n            if (!$user) {\n                throw $this->createNotFoundException('User not found');\n            }\n            $oldHash = $user->getPassword();\n            $principal = $doctrine->getRepository(Principal::class)->findOneByUri(Principal::PREFIX.$user->getUsername());\n        } else {\n            $user = new User();\n            $principal = new Principal();\n        }\n\n        $form = $this->createForm(UserType::class, $user, ['new' => !$userId]);\n\n        $form->get('displayName')->setData($principal->getDisplayName());\n        $form->get('email')->setData($principal->getEmail());\n        $form->get('isAdmin')->setData($principal->getIsAdmin());\n\n        $form->handleRequest($request);\n\n        if ($form->isSubmitted() && $form->isValid()) {\n            $displayName = $form->get('displayName')->getData();\n            $email = $form->get('email')->getData();\n            $isAdmin = $form->get('isAdmin')->getData();\n\n            // Create password for user\n            if ($userId && is_null($user->getPassword())) {\n                // The user is not new and does not want to change its password\n                $user->setPassword($oldHash);\n            } else {\n                $hash = password_hash($user->getPassword(), PASSWORD_DEFAULT);\n                $user->setPassword($hash);\n            }\n\n            $entityManager = $doctrine->getManager();\n\n            // If it's a new user, create default calendar and address book, and principal\n            if (null === $user->getId()) {\n                $principal->setUri(Principal::PREFIX.$user->getUsername());\n\n                $calendarInstance = new CalendarInstance();\n                $calendar = new Calendar();\n                $calendarInstance->setPrincipalUri(Principal::PREFIX.$user->getUsername())\n                         ->setUri('default') // No risk of collision since unicity is guaranteed by the new user principal\n                         ->setDisplayName($trans->trans('default.calendar.title'))\n                         ->setDescription($trans->trans('default.calendar.description', ['user' => $displayName]))\n                         ->setCalendar($calendar);\n\n                // Enable delegation by default\n                $principalProxyRead = new Principal();\n                $principalProxyRead->setUri($principal->getUri().Principal::READ_PROXY_SUFFIX)\n                                   ->setIsMain(false);\n                $entityManager->persist($principalProxyRead);\n\n                $principalProxyWrite = new Principal();\n                $principalProxyWrite->setUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX)\n                                   ->setIsMain(false);\n                $entityManager->persist($principalProxyWrite);\n\n                $addressbook = new AddressBook();\n                $addressbook->setPrincipalUri(Principal::PREFIX.$user->getUsername())\n                         ->setUri('default') // No risk of collision since unicity is guaranteed by the new user principal\n                         ->setDisplayName($trans->trans('default.addressbook.title'))\n                         ->setDescription($trans->trans('default.addressbook.description', ['user' => $displayName]));\n                $entityManager->persist($calendarInstance);\n                $entityManager->persist($addressbook);\n                $entityManager->persist($principal);\n            }\n\n            $principal->setDisplayName($displayName)\n                      ->setEmail($email)\n                      ->setIsAdmin($isAdmin);\n\n            $entityManager->persist($user);\n            $entityManager->flush();\n\n            $this->addFlash('success', $trans->trans('user.saved'));\n\n            return $this->redirectToRoute('user_index');\n        }\n\n        return $this->render('users/edit.html.twig', [\n            'form' => $form->createView(),\n            'userId' => $userId,\n            'username' => $user->getUsername(),\n        ]);\n    }\n\n    #[Route('/delete/{userId}', name: 'delete')]\n    public function userDelete(ManagerRegistry $doctrine, int $userId, TranslatorInterface $trans): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $entityManager = $doctrine->getManager();\n        $entityManager->remove($user);\n\n        $principal = $doctrine->getRepository(Principal::class)->findOneByUri(Principal::PREFIX.$user->getUsername());\n        $principalProxyRead = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::READ_PROXY_SUFFIX);\n        $principalProxyWrite = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX);\n\n        $entityManager->remove($principal);\n        $entityManager->remove($principalProxyRead);\n        $entityManager->remove($principalProxyWrite);\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        // Remove calendars and addressbooks\n        $calendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUri($principalUri);\n        foreach ($calendars ?? [] as $instance) {\n            // We're only removing the calendar objects / changes / and calendar if the deleted user is an owner,\n            // which means that the underlying calendar instance should not have another principal as owner.\n            $hasDifferentOwner = $doctrine->getRepository(CalendarInstance::class)->hasDifferentOwner($instance->getCalendar()->getId(), $principalUri);\n            if (!$hasDifferentOwner) {\n                foreach ($instance->getCalendar()->getObjects() ?? [] as $object) {\n                    $entityManager->remove($object);\n                }\n                foreach ($instance->getCalendar()->getChanges() ?? [] as $change) {\n                    $entityManager->remove($change);\n                }\n                // We need to remove the shared versions of this calendar, too\n                foreach ($instance->getCalendar()->getInstances() ?? [] as $instances) {\n                    $entityManager->remove($instances);\n                }\n                $entityManager->remove($instance->getCalendar());\n            }\n            $entityManager->remove($instance);\n        }\n        $calendarsSubscriptions = $doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($principalUri);\n        foreach ($calendarsSubscriptions ?? [] as $subscription) {\n            $entityManager->remove($subscription);\n        }\n        $schedulingObjects = $doctrine->getRepository(SchedulingObject::class)->findByPrincipalUri($principalUri);\n        foreach ($schedulingObjects ?? [] as $object) {\n            $entityManager->remove($object);\n        }\n\n        $addressbooks = $doctrine->getRepository(AddressBook::class)->findByPrincipalUri($principalUri);\n        foreach ($addressbooks ?? [] as $addressbook) {\n            foreach ($addressbook->getCards() ?? [] as $card) {\n                $entityManager->remove($card);\n            }\n            foreach ($addressbook->getChanges() ?? [] as $change) {\n                $entityManager->remove($change);\n            }\n            $entityManager->remove($addressbook);\n        }\n\n        $entityManager->flush();\n        $this->addFlash('success', $trans->trans('user.deleted'));\n\n        return $this->redirectToRoute('user_index');\n    }\n\n    #[Route('/delegates/{userId}', name: 'delegates')]\n    public function userDelegates(ManagerRegistry $doctrine, int $userId): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri);\n\n        $allPrincipalsExcept = $doctrine->getRepository(Principal::class)->findAllExceptPrincipal($principalUri);\n\n        // Get delegates. They are not linked to the principal in itself, but to its proxies\n        $principalProxyRead = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::READ_PROXY_SUFFIX);\n        $principalProxyWrite = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX);\n\n        return $this->render('users/delegates.html.twig', [\n            'principal' => $principal,\n            'userId' => $userId,\n            'delegation' => $principalProxyRead && $principalProxyWrite,\n            'principalProxyRead' => $principalProxyRead,\n            'principalProxyWrite' => $principalProxyWrite,\n            'allPrincipals' => $allPrincipalsExcept,\n        ]);\n    }\n\n    #[Route('/delegation/{userId}/{toggle}', name: 'delegation_toggle', requirements: ['toggle' => '(on|off)'])]\n    public function userToggleDelegation(ManagerRegistry $doctrine, int $userId, string $toggle): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri);\n\n        if (!$principal) {\n            throw $this->createNotFoundException('Principal not found');\n        }\n\n        $entityManager = $doctrine->getManager();\n\n        if ('on' === $toggle) {\n            $principalProxyRead = new Principal();\n            $principalProxyRead->setUri($principal->getUri().Principal::READ_PROXY_SUFFIX)\n                               ->setIsMain(false);\n            $entityManager->persist($principalProxyRead);\n\n            $principalProxyWrite = new Principal();\n            $principalProxyWrite->setUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX)\n                               ->setIsMain(false);\n            $entityManager->persist($principalProxyWrite);\n        } else {\n            $principalProxyRead = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::READ_PROXY_SUFFIX);\n            $principalProxyRead && $entityManager->remove($principalProxyRead);\n\n            $principalProxyWrite = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX);\n            $principalProxyWrite && $entityManager->remove($principalProxyWrite);\n\n            // Remove also delegates\n            $principal->removeAllDelegees();\n        }\n\n        $entityManager->flush();\n\n        return $this->redirectToRoute('user_delegates', ['userId' => $userId]);\n    }\n\n    #[Route('/delegates/{userId}/add', name: 'delegate_add')]\n    public function userDelegateAdd(ManagerRegistry $doctrine, Request $request, int $userId): Response\n    {\n        if (!is_numeric($request->get('principalId'))) {\n            throw new BadRequestHttpException();\n        }\n\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        $newMemberToAdd = $doctrine->getRepository(Principal::class)->findOneById($request->get('principalId'));\n\n        if (!$newMemberToAdd) {\n            throw $this->createNotFoundException('Member not found');\n        }\n\n        // Depending on write access or not, attach to the correct principal\n        if ('true' === $request->get('write')) {\n            // Let's check that there wasn't a read proxy first\n            $principalProxyRead = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri.Principal::READ_PROXY_SUFFIX);\n            if (!$principalProxyRead) {\n                throw $this->createNotFoundException('Principal linked to this calendar not found');\n            }\n            $principalProxyRead->removeDelegee($newMemberToAdd);\n            // And then add the Write access\n            $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri.Principal::WRITE_PROXY_SUFFIX);\n        } else {\n            $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri.Principal::READ_PROXY_SUFFIX);\n        }\n\n        if (!$principal) {\n            throw $this->createNotFoundException('Principal linked to this calendar not found');\n        }\n\n        $principal->addDelegee($newMemberToAdd);\n        $entityManager = $doctrine->getManager();\n        $entityManager->flush();\n\n        return $this->redirectToRoute('user_delegates', ['userId' => $userId]);\n    }\n\n    #[Route('/delegates/{userId}/remove/{principalProxyId}/{delegateId}', name: 'delegate_remove', requirements: ['principalProxyId' => \"\\d+\", 'delegateId' => \"\\d+\"])]\n    public function userDelegateRemove(ManagerRegistry $doctrine, Request $request, int $userId, int $principalProxyId, int $delegateId): Response\n    {\n        $user = $doctrine->getRepository(User::class)->findOneById($userId);\n        if (!$user) {\n            throw $this->createNotFoundException('User not found');\n        }\n\n        $principalProxy = $doctrine->getRepository(Principal::class)->findOneById($principalProxyId);\n        if (!$principalProxy) {\n            throw $this->createNotFoundException('Principal linked to this calendar not found');\n        }\n\n        $memberToRemove = $doctrine->getRepository(Principal::class)->findOneById($delegateId);\n        if (!$memberToRemove) {\n            throw $this->createNotFoundException('Member not found');\n        }\n\n        $principalProxy->removeDelegee($memberToRemove);\n        $entityManager = $doctrine->getManager();\n        $entityManager->flush();\n\n        return $this->redirectToRoute('user_delegates', ['userId' => $userId]);\n    }\n}\n"
  },
  {
    "path": "src/Controller/Api/ApiController.php",
    "content": "<?php\n\nnamespace App\\Controller\\Api;\n\nuse App\\Entity\\Calendar;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\CalendarSubscription;\nuse App\\Entity\\Principal;\nuse App\\Entity\\SchedulingObject;\nuse App\\Entity\\User;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Sabre\\DAV\\Sharing\\Plugin as SharingPlugin;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\n\n#[Route('/api/v1', name: 'api_v1_')]\nclass ApiController extends AbstractController\n{\n    /**\n     * Validates the provided username.\n     *\n     * @param string $username The username to validate\n     *\n     * @return bool True if the username is valid, false otherwise\n     */\n    private function validateUsername(string $username): bool\n    {\n        return !empty($username) && is_string($username) && !preg_match('/[^a-zA-Z0-9_.@-]/', $username);\n    }\n\n    /**\n     * Gets the current timestamp in ISO 8601 format.\n     *\n     * @return string The current timestamp\n     */\n    private function getTimestamp(): string\n    {\n        return date('c');\n    }\n\n    /**\n     * Resolves a User entity from a userId, or returns a JSON error response.\n     *\n     * @return User|null The User entity, or null if not found\n     */\n    private function resolveUser(ManagerRegistry $doctrine, int $userId): ?User\n    {\n        return $doctrine->getRepository(User::class)->findOneById($userId);\n    }\n\n    /**\n     * Health check endpoint.\n     *\n     * @param Request $request The HTTP GET request\n     *\n     * @return JsonResponse A JSON response indicating the health status\n     */\n    #[Route('/health', name: 'health', methods: ['GET'])]\n    public function healthCheck(Request $request): JsonResponse\n    {\n        return $this->json(['status' => 'OK', 'timestamp' => $this->getTimestamp()], 200);\n    }\n\n    /**\n     * Retrieves a list of users (with their user_id, principal_id, uri, username, and displayname).\n     *\n     * @param Request $request The HTTP GET request\n     *\n     * @return JsonResponse A JSON response containing the list of users\n     */\n    #[Route('/users', name: 'users', methods: ['GET'])]\n    public function getUsers(Request $request, ManagerRegistry $doctrine): JsonResponse\n    {\n        $results = $doctrine->getRepository(Principal::class)->findAllMainPrincipalsWithUserIds();\n\n        $users = [];\n        foreach ($results as $result) {\n            $principal = $result[0];\n            $users[] = [\n                'user_id' => $result['userId'],\n                'principal_id' => $principal->getId(),\n                'uri' => $principal->getUri(),\n                'username' => $principal->getUsername(),\n            ];\n        }\n\n        $response = [\n            'status' => 'success',\n            'data' => $users,\n            'timestamp' => $this->getTimestamp(),\n        ];\n\n        return $this->json($response, 200);\n    }\n\n    /**\n     * Retrieves details of a specific user (user_id, principal_id, uri, username, displayname, email).\n     *\n     * @param Request $request The HTTP GET request\n     * @param int     $userId  The ID of the user whose details are to be retrieved\n     *\n     * @return JsonResponse A JSON response containing the user details\n     */\n    #[Route('/users/{userId}', name: 'user_detail', methods: ['GET'], requirements: ['userId' => '\\d+'])]\n    public function getUserDetails(Request $request, ManagerRegistry $doctrine, int $userId): JsonResponse\n    {\n        $user = $this->resolveUser($doctrine, $userId);\n        if (!$user) {\n            return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $principal = $doctrine->getRepository(Principal::class)->findOneByUri(Principal::PREFIX.$user->getUsername());\n\n        if (!$principal) {\n            return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $data = [\n            'user_id' => $user->getId(),\n            'principal_id' => $principal->getId(),\n            'uri' => $principal->getUri(),\n            'username' => $principal->getUsername(),\n            'displayname' => $principal->getDisplayName(),\n            'email' => $principal->getEmail(),\n        ];\n\n        $response = [\n            'status' => 'success',\n            'data' => $data,\n            'timestamp' => $this->getTimestamp(),\n        ];\n\n        return $this->json($response, 200);\n    }\n\n    /**\n     * Retrieves a list of calendars for a specific user, including user calendars, shared calendars, and subscriptions.\n     *\n     * @param Request $request The HTTP GET request\n     * @param int     $userId  The ID of the user whose calendars are to be retrieved\n     *\n     * @return JsonResponse A JSON response containing the list of calendars for the specified user\n     */\n    #[Route('/calendars/{userId}', name: 'calendars', methods: ['GET'], requirements: ['userId' => '\\d+'])]\n    public function getUserCalendars(Request $request, int $userId, ManagerRegistry $doctrine): JsonResponse\n    {\n        $user = $this->resolveUser($doctrine, $userId);\n        if (!$user) {\n            return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        if (!$doctrine->getRepository(Principal::class)->findOneByUri($principalUri)) {\n            return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $allCalendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUri($principalUri);\n        $allSubscriptions = $doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($principalUri);\n\n        $calendars = [];\n        $sharedCalendars = [];\n        foreach ($allCalendars as $calendar) {\n            $objectCounts = $doctrine->getRepository(CalendarInstance::class)->getObjectCountsByComponentType($calendar->getCalendar()->getId());\n            $eventsCount = $calendar->getCalendar()->isComponentEnabled(Calendar::COMPONENT_EVENTS) ? $objectCounts['events'] : null;\n            $notesCount = $calendar->getCalendar()->isComponentEnabled(Calendar::COMPONENT_NOTES) ? $objectCounts['notes'] : null;\n            $tasksCount = $calendar->getCalendar()->isComponentEnabled(Calendar::COMPONENT_TODOS) ? $objectCounts['tasks'] : null;\n\n            $calendarData = [\n                'id' => $calendar->getId(),\n                'uri' => $calendar->getUri(),\n                'displayname' => $calendar->getDisplayName(),\n                'events' => $eventsCount,\n                'notes' => $notesCount,\n                'tasks' => $tasksCount,\n            ];\n            if (!$calendar->isShared()) {\n                $calendars[] = $calendarData;\n            } else {\n                $sharedCalendars[] = $calendarData;\n            }\n        }\n\n        $subscriptions = [];\n        foreach ($allSubscriptions as $subscription) {\n            $objectCounts = $doctrine->getRepository(CalendarInstance::class)->getObjectCountsByComponentType($subscription->getCalendar()->getId());\n            $eventsCount = $subscription->getCalendar()->isComponentEnabled(Calendar::COMPONENT_EVENTS) ? $objectCounts['events'] : null;\n            $notesCount = $subscription->getCalendar()->isComponentEnabled(Calendar::COMPONENT_NOTES) ? $objectCounts['notes'] : null;\n            $tasksCount = $subscription->getCalendar()->isComponentEnabled(Calendar::COMPONENT_TODOS) ? $objectCounts['tasks'] : null;\n\n            $subscriptions[] = [\n                'id' => $subscription->getId(),\n                'uri' => $subscription->getUri(),\n                'displayname' => $subscription->getDisplayName(),\n                'events' => $eventsCount,\n                'notes' => $notesCount,\n                'tasks' => $tasksCount,\n            ];\n        }\n\n        $response = [\n            'status' => 'success',\n            'data' => [\n                'user_calendars' => $calendars,\n                'shared_calendars' => $sharedCalendars,\n                'subscriptions' => $subscriptions,\n            ],\n            'timestamp' => $this->getTimestamp(),\n        ];\n\n        return $this->json($response, 200);\n    }\n\n    /**\n     * Retrieves details of a specific calendar for a specific user (id, uri, displayname, description, number of events, notes, and tasks).\n     *\n     * @param Request $request     The HTTP GET request\n     * @param int     $userId      The ID of the user whose calendar details are to be retrieved\n     * @param int     $calendar_id The ID of the calendar whose details are to be retrieved\n     *\n     * @return JsonResponse A JSON response containing the calendar details\n     */\n    #[Route('/calendars/{userId}/{calendar_id}', name: 'calendar_details', methods: ['GET'], requirements: ['calendar_id' => '\\d+', 'userId' => '\\d+'])]\n    public function getUserCalendarDetails(Request $request, int $userId, int $calendar_id, ManagerRegistry $doctrine): JsonResponse\n    {\n        $user = $this->resolveUser($doctrine, $userId);\n        if (!$user) {\n            return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        if (!$doctrine->getRepository(Principal::class)->findOneByUri($principalUri)) {\n            return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $allCalendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUri($principalUri);\n\n        $calendar_details = [];\n        foreach ($allCalendars as $calendar) {\n            if (!$calendar->isShared() && $calendar->getId() === $calendar_id) {\n                $objectCounts = $doctrine->getRepository(CalendarInstance::class)->getObjectCountsByComponentType($calendar->getCalendar()->getId());\n                $calendar_details = [\n                    'id' => $calendar->getId(),\n                    'uri' => $calendar->getUri(),\n                    'displayname' => $calendar->getDisplayName(),\n                    'description' => $calendar->getDescription(),\n                    'events' => [\n                        'enabled' => $calendar->getCalendar()->isComponentEnabled(Calendar::COMPONENT_EVENTS),\n                        'count' => $objectCounts['events'],\n                    ],\n                    'notes' => [\n                        'enabled' => $calendar->getCalendar()->isComponentEnabled(Calendar::COMPONENT_NOTES),\n                        'count' => $objectCounts['notes'],\n                    ],\n                    'tasks' => [\n                        'enabled' => $calendar->getCalendar()->isComponentEnabled(Calendar::COMPONENT_TODOS),\n                        'count' => $objectCounts['tasks'],\n                    ],\n                ];\n            }\n        }\n\n        $response = [\n            'status' => 'success',\n            'data' => $calendar_details,\n            'timestamp' => $this->getTimestamp(),\n        ];\n\n        return $this->json($response, 200);\n    }\n\n    /**\n     * Creates a new calendar for a specific user.\n     *\n     * @param Request $request The HTTP POST request\n     * @param int     $userId  The ID of the user for whom the calendar is to be created\n     *\n     * @return JsonResponse A JSON response indicating the success or failure of the operation\n     */\n    #[Route('/calendars/{userId}/create', name: 'calendar_create', methods: ['POST'], requirements: ['userId' => '\\d+'])]\n    public function createNewUserCalendar(Request $request, int $userId, ManagerRegistry $doctrine): JsonResponse\n    {\n        $user = $this->resolveUser($doctrine, $userId);\n        if (!$user) {\n            return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        if (!$doctrine->getRepository(Principal::class)->findOneByUri($principalUri)) {\n            return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        // Parse JSON body\n        $data = json_decode($request->getContent(), true);\n        if (JSON_ERROR_NONE !== json_last_error()) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid JSON', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $calendarName = $data['name'] ?? null;\n        if (empty($calendarName) || 1 !== preg_match('/^[a-zA-Z0-9 ._-]{1,64}$/', $calendarName)) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Calendar Name', 'timestamp' => $this->getTimestamp()], 400);\n        }\n        $calendarURI = $data['uri'] ?? null;\n        if (empty($calendarURI) || 1 !== preg_match('/^[a-z0-9_-]{1,128}$/', $calendarURI)) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Calendar URI', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $uriCheck = $doctrine->getRepository(CalendarInstance::class)->findOneBy([\n            'principalUri' => $principalUri,\n            'uri' => $calendarURI,\n        ]);\n        if ($uriCheck) {\n            return $this->json(['status' => 'error', 'message' => 'Calendar URI Already Exists', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $calendarDescription = $data['description'] ?? '';\n        if (!empty($calendarDescription) && 1 !== preg_match('/^[a-zA-Z0-9 ._-]{1,256}$/', $calendarDescription)) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Calendar Description', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $entityManager = $doctrine->getManager();\n        $calendarInstance = new CalendarInstance();\n        $calendar = new Calendar();\n        $calendarInstance->setCalendar($calendar);\n\n        $calendarComponents = [];\n        $eventsSupport = $data['events_support'] ?? true;\n        if (true === $eventsSupport || 'true' === $eventsSupport) {\n            $calendarComponents[] = Calendar::COMPONENT_EVENTS;\n        }\n        $notesSupport = $data['notes_support'] ?? false;\n        if (true === $notesSupport || 'true' === $notesSupport) {\n            $calendarComponents[] = Calendar::COMPONENT_NOTES;\n        }\n        $tasksSupport = $data['tasks_support'] ?? false;\n        if (true === $tasksSupport || 'true' === $tasksSupport) {\n            $calendarComponents[] = Calendar::COMPONENT_TODOS;\n        }\n\n        // Validate that at least one component is selected\n        if (empty($calendarComponents)) {\n            return $this->json(['status' => 'error', 'message' => 'At least one calendar component must be enabled (events, notes, or tasks)', 'timestamp' => $this->getTimestamp()], 400);\n        }\n        $calendar->setComponents(implode(',', $calendarComponents));\n\n        try {\n            $calendarInstance\n            ->setCalendar($calendar)\n            ->setAccess(SharingPlugin::ACCESS_SHAREDOWNER)\n            ->setDescription($calendarDescription)\n            ->setDisplayName($calendarName)\n            ->setUri($calendarURI)\n            ->setPrincipalUri($principalUri);\n\n            $entityManager->persist($calendarInstance);\n            $entityManager->flush();\n        } catch (\\Exception $e) {\n            return $this->json(['status' => 'error', 'message' => 'Failed to Create Calendar', 'timestamp' => $this->getTimestamp()], 500);\n        }\n\n        $response = [\n            'status' => 'success',\n            'data' => [\n                'calendar_id' => $calendarInstance->getId(),\n                'calendar_uri' => $calendarInstance->getUri(),\n            ],\n            'timestamp' => $this->getTimestamp(),\n        ];\n\n        return $this->json($response, 200);\n    }\n\n    /**\n     * Edits an existing calendar for a specific user.\n     *\n     * @param Request $request     The HTTP POST request\n     * @param int     $userId      The ID of the user whose calendar is to be edited\n     * @param int     $calendar_id The ID of the calendar to be edited\n     *\n     * @return JsonResponse A JSON response indicating the success or failure of the operation\n     */\n    #[Route('/calendars/{userId}/{calendar_id}', name: 'calendar_edit', methods: ['PUT', 'PATCH'], requirements: ['calendar_id' => '\\d+', 'userId' => '\\d+'])]\n    public function editUserCalendar(Request $request, int $userId, int $calendar_id, ManagerRegistry $doctrine): JsonResponse\n    {\n        $user = $this->resolveUser($doctrine, $userId);\n        if (!$user) {\n            return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        if (!$doctrine->getRepository(Principal::class)->findOneByUri($principalUri)) {\n            return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $ownerInstance = $doctrine->getRepository(CalendarInstance::class)->findOneBy([\n            'id' => $calendar_id,\n            'principalUri' => $principalUri,\n        ]);\n\n        if (!$ownerInstance) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Calendar ID', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $calendarInstance = $doctrine->getRepository(CalendarInstance::class)->findOneById($calendar_id);\n        if (!$calendarInstance) {\n            return $this->json(['status' => 'error', 'message' => 'Calendar Instance Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        // Parse JSON body\n        $data = json_decode($request->getContent(), true);\n        if (JSON_ERROR_NONE !== json_last_error()) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid JSON', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $calendarName = $data['name'] ?? null;\n        if (empty($calendarName) || 1 !== preg_match('/^[a-zA-Z0-9 ._-]{1,64}$/', $calendarName)) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Calendar Name', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $calendarDescription = $data['description'] ?? '';\n        if (!empty($calendarDescription) && 1 !== preg_match('/^[a-zA-Z0-9 ._-]{1,256}$/', $calendarDescription)) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Calendar Description', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $entityManager = $doctrine->getManager();\n        $calendarInstance->setDisplayName($calendarName);\n        $calendarInstance->setDescription($calendarDescription);\n\n        $calendarComponents = [];\n        $eventsSupport = $data['events_support'] ?? true;\n        if (true === $eventsSupport || 'true' === $eventsSupport) {\n            $calendarComponents[] = Calendar::COMPONENT_EVENTS;\n        }\n        $notesSupport = $data['notes_support'] ?? false;\n        if (true === $notesSupport || 'true' === $notesSupport) {\n            $calendarComponents[] = Calendar::COMPONENT_NOTES;\n        }\n        $tasksSupport = $data['tasks_support'] ?? false;\n        if (true === $tasksSupport || 'true' === $tasksSupport) {\n            $calendarComponents[] = Calendar::COMPONENT_TODOS;\n        }\n\n        // Validate that at least one component is selected\n        if (empty($calendarComponents)) {\n            return $this->json(['status' => 'error', 'message' => 'At least one calendar component must be enabled (events, notes, or tasks)', 'timestamp' => $this->getTimestamp()], 400);\n        }\n        $calendarInstance->getCalendar()->setComponents(implode(',', $calendarComponents));\n\n        try {\n            $entityManager->persist($calendarInstance);\n            $entityManager->flush();\n        } catch (\\Exception $e) {\n            return $this->json(['status' => 'error', 'message' => 'Failed to Edit Calendar', 'timestamp' => $this->getTimestamp()], 500);\n        }\n\n        return $this->json(['status' => 'success', 'timestamp' => $this->getTimestamp()], 200);\n    }\n\n    /**\n     * Deletes a specific calendar for a specific user.\n     *\n     * @param Request $request     The HTTP POST request\n     * @param int     $userId      The ID of the user whose calendar is to be deleted\n     * @param int     $calendar_id The ID of the calendar to be deleted\n     *\n     * @return JsonResponse A JSON response indicating the success or failure of the operation\n     */\n    #[Route('/calendars/{userId}/{calendar_id}', name: 'calendar_delete', methods: ['DELETE'], requirements: ['calendar_id' => '\\d+', 'userId' => '\\d+'])]\n    public function deleteUserCalendar(Request $request, int $userId, int $calendar_id, ManagerRegistry $doctrine): JsonResponse\n    {\n        $user = $this->resolveUser($doctrine, $userId);\n        if (!$user) {\n            return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        if (!$doctrine->getRepository(Principal::class)->findOneByUri($principalUri)) {\n            return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $instance = $doctrine->getRepository(CalendarInstance::class)->findOneBy([\n            'id' => $calendar_id,\n            'principalUri' => $principalUri,\n        ]);\n\n        if (!$instance) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Instance Not Found', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        try {\n            $entityManager = $doctrine->getManager();\n            $calendarsSubscriptions = $doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($instance->getPrincipalUri());\n            $schedulingObjects = $doctrine->getRepository(SchedulingObject::class)->findByPrincipalUri($instance->getPrincipalUri());\n\n            // Remove calendar objects\n            foreach ($calendarsSubscriptions ?? [] as $subscription) {\n                $entityManager->remove($subscription);\n            }\n            foreach ($schedulingObjects ?? [] as $object) {\n                $entityManager->remove($object);\n            }\n            foreach ($instance->getCalendar()->getObjects() ?? [] as $object) {\n                $entityManager->remove($object);\n            }\n            foreach ($instance->getCalendar()->getChanges() ?? [] as $change) {\n                $entityManager->remove($change);\n            }\n\n            // Remove the original calendar instance\n            $entityManager->remove($instance);\n\n            // Remove shared instances of the calendar\n            $sharedInstances = $doctrine->getRepository(CalendarInstance::class)->findSharedInstancesOfInstance($instance->getCalendar()->getId(), false);\n            foreach ($sharedInstances as $sharedInstance) {\n                $entityManager->remove($sharedInstance);\n            }\n\n            $entityManager->remove($instance->getCalendar());\n            $entityManager->flush();\n        } catch (\\Exception $e) {\n            return $this->json(['status' => 'error', 'message' => 'Failed to Delete Calendar', 'timestamp' => $this->getTimestamp()], 500);\n        }\n\n        return $this->json(['status' => 'success', 'timestamp' => $this->getTimestamp()], 200);\n    }\n\n    /**\n     * Retrieves a list of shares for a specific calendar of a specific user (id, username, displayname, email, write_access).\n     *\n     * @param Request $request     The HTTP GET request\n     * @param int     $userId      The ID of the user whose calendar shares are to be retrieved\n     * @param int     $calendar_id The ID of the calendar whose shares are to be retrieved\n     *\n     * @return JsonResponse A JSON response containing the list of calendar shares\n     */\n    #[Route('/calendars/{userId}/shares/{calendar_id}', name: 'calendars_shares', methods: ['GET'], requirements: ['calendar_id' => '\\d+', 'userId' => '\\d+'])]\n    public function getUserCalendarsShares(Request $request, int $userId, int $calendar_id, ManagerRegistry $doctrine): JsonResponse\n    {\n        $user = $this->resolveUser($doctrine, $userId);\n        if (!$user) {\n            return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        if (!$doctrine->getRepository(Principal::class)->findOneByUri($principalUri)) {\n            return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $ownerInstance = $doctrine->getRepository(CalendarInstance::class)->findOneBy([\n            'id' => $calendar_id,\n            'principalUri' => $principalUri,\n        ]);\n\n        if (!$ownerInstance) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Calendar ID/Username', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        // This fixes the issue where shared calendars are not being retrieved properly\n        $instances = $doctrine->getRepository(CalendarInstance::class)->findSharedInstancesOfInstance($ownerInstance->getCalendar()->getId(), true);\n\n        $calendars = [];\n        foreach ($instances as $instance) {\n            $principalId = $doctrine->getRepository(Principal::class)->findOneByUri($instance[0]['principalUri']);\n\n            $instanceUsername = mb_substr($instance[0]['principalUri'], strlen(Principal::PREFIX));\n            $instanceUserId = $doctrine->getRepository(User::class)->findOneByUsername($instanceUsername)->getId();\n\n            $calendars[] = [\n                'username' => $instanceUsername,\n                'user_id' => $instanceUserId,\n                'principal_id' => $principalId?->getId() ?? null,\n                'displayname' => $instance['displayName'],\n                'email' => $instance['email'],\n                'write_access' => SharingPlugin::ACCESS_READWRITE === $instance[0]['access'],\n            ];\n        }\n\n        $response = [\n            'status' => 'success',\n            'data' => $calendars,\n            'timestamp' => $this->getTimestamp(),\n        ];\n\n        return $this->json($response, 200);\n    }\n\n    /**\n     * Sets or updates a share for a specific calendar of a specific user.\n     *\n     * @param Request $request     The HTTP POST request\n     * @param int     $userId      The ID of the user whose calendar share is to be set or updated\n     * @param string  $calendar_id The ID of the calendar whose share is to be set or updated\n     *\n     * @return JsonResponse A JSON response indicating the success or failure of the operation\n     */\n    #[Route('/calendars/{userId}/share/{calendar_id}/add', name: 'calendars_share', methods: ['POST'], requirements: ['calendar_id' => '\\d+', 'userId' => '\\d+'])]\n    public function setUserCalendarsShare(Request $request, int $userId, int $calendar_id, ManagerRegistry $doctrine): JsonResponse\n    {\n        $user = $this->resolveUser($doctrine, $userId);\n        if (!$user) {\n            return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        if (!$doctrine->getRepository(Principal::class)->findOneByUri($principalUri)) {\n            return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $ownerInstance = $doctrine->getRepository(CalendarInstance::class)->findOneBy([\n            'id' => $calendar_id,\n            'principalUri' => $principalUri,\n        ]);\n\n        if (!$ownerInstance) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Calendar ID and User ID', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        // Parse JSON body\n        $data = json_decode($request->getContent(), true);\n        if (JSON_ERROR_NONE !== json_last_error()) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid JSON', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $shareeUsername = $data['username'] ?? null;\n        $writeAccess = $data['write_access'] ?? null;\n        if (!$this->validateUsername($shareeUsername) || !in_array($writeAccess, [true, false, 'true', 'false'], true)) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Sharee ID/Write Access Value', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $instance = $doctrine->getRepository(CalendarInstance::class)->findOneById($calendar_id);\n        $newShareeToAdd = $doctrine->getRepository(Principal::class)->findOneByUri(Principal::PREFIX.$shareeUsername);\n\n        if (!$instance || !$newShareeToAdd) {\n            return $this->json(['status' => 'error', 'message' => 'Calendar Instance/User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $existingSharedInstance = $doctrine->getRepository(CalendarInstance::class)->findSharedInstanceOfInstanceFor($instance->getCalendar()->getId(), $newShareeToAdd->getUri());\n        $accessLevel = (true === $writeAccess || 'true' === $writeAccess ? SharingPlugin::ACCESS_READWRITE : SharingPlugin::ACCESS_READ);\n        $entityManager = $doctrine->getManager();\n\n        try {\n            if ($existingSharedInstance) {\n                $existingSharedInstance->setAccess($accessLevel);\n            } else {\n                $sharedInstance = new CalendarInstance();\n                $sharedInstance->setTransparent(1)\n                        ->setCalendar($instance->getCalendar())\n                        ->setShareHref('mailto:'.$newShareeToAdd->getEmail())\n                        ->setDescription($instance->getDescription())\n                        ->setDisplayName($instance->getDisplayName())\n                        ->setUri(\\Sabre\\DAV\\UUIDUtil::getUUID())\n                        ->setPrincipalUri($newShareeToAdd->getUri())\n                        ->setAccess($accessLevel);\n                $entityManager->persist($sharedInstance);\n            }\n            $entityManager->flush();\n        } catch (\\Exception $e) {\n            return $this->json(['status' => 'error', 'message' => 'Failed to Edit Calendar', 'timestamp' => $this->getTimestamp()], 500);\n        }\n\n        return $this->json(['status' => 'success', 'timestamp' => $this->getTimestamp()], 200);\n    }\n\n    /**\n     * Removes a share for a specific calendar of a specific user.\n     *\n     * @param Request $request     The HTTP POST request\n     * @param int     $userId      The ID of the user whose calendar share is to be removed\n     * @param string  $calendar_id The ID of the calendar whose share is to be removed\n     *\n     * @return JsonResponse A JSON response indicating the success or failure of the operation\n     */\n    #[Route('/calendars/{userId}/share/{calendar_id}/remove', name: 'calendars_share_remove', methods: ['POST'], requirements: ['calendar_id' => '\\d+', 'userId' => '\\d+'])]\n    public function removeUserCalendarsShare(Request $request, int $userId, int $calendar_id, ManagerRegistry $doctrine): JsonResponse\n    {\n        $user = $this->resolveUser($doctrine, $userId);\n        if (!$user) {\n            return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $principalUri = Principal::PREFIX.$user->getUsername();\n\n        if (!$doctrine->getRepository(Principal::class)->findOneByUri($principalUri)) {\n            return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        $ownerInstance = $doctrine->getRepository(CalendarInstance::class)->findOneBy([\n            'id' => $calendar_id,\n            'principalUri' => $principalUri,\n        ]);\n\n        if (!$ownerInstance) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Calendar ID', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        // Parse JSON body\n        $data = json_decode($request->getContent(), true);\n        if (JSON_ERROR_NONE !== json_last_error()) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid JSON', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $shareeUsername = $data['username'] ?? null;\n        if (!$this->validateUsername($shareeUsername)) {\n            return $this->json(['status' => 'error', 'message' => 'Invalid Username', 'timestamp' => $this->getTimestamp()], 400);\n        }\n\n        $instance = $doctrine->getRepository(CalendarInstance::class)->findOneById($calendar_id);\n        $shareeToRemove = $doctrine->getRepository(Principal::class)->findOneByUri(Principal::PREFIX.$shareeUsername);\n\n        if (!$instance || !$shareeToRemove) {\n            return $this->json(['status' => 'error', 'message' => 'Calendar Instance/User Not Found', 'timestamp' => $this->getTimestamp()], 404);\n        }\n\n        try {\n            $existingSharedInstance = $doctrine->getRepository(CalendarInstance::class)->findSharedInstanceOfInstanceFor($instance->getCalendar()->getId(), $shareeToRemove->getUri());\n\n            if ($existingSharedInstance) {\n                $entityManager = $doctrine->getManager();\n                $entityManager->remove($existingSharedInstance);\n                $entityManager->flush();\n            }\n        } catch (\\Exception $e) {\n            return $this->json(['status' => 'error', 'message' => 'Failed to Remove Share', 'timestamp' => $this->getTimestamp()], 500);\n        }\n\n        return $this->json(['status' => 'success', 'timestamp' => $this->getTimestamp()], 200);\n    }\n}\n"
  },
  {
    "path": "src/Controller/DAVController.php",
    "content": "<?php\n\nnamespace App\\Controller;\n\nuse App\\Entity\\Principal;\nuse App\\Entity\\User;\nuse App\\Plugins\\BirthdayCalendarPlugin;\nuse App\\Plugins\\DavisIMipPlugin;\nuse App\\Plugins\\PublicAwareDAVACLPlugin;\nuse App\\Services\\BasicAuth;\nuse App\\Services\\BirthdayService;\nuse App\\Services\\IMAPAuth;\nuse App\\Services\\LDAPAuth;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse PDO;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\HttpKernel\\Profiler\\Profiler;\nuse Symfony\\Component\\Mailer\\MailerInterface;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\n\nclass DAVController extends AbstractController\n{\n    public const AUTH_BASIC = 'Basic';\n    public const AUTH_IMAP = 'IMAP';\n    public const AUTH_LDAP = 'LDAP';\n\n    /**\n     * Is CalDAV enabled?\n     *\n     * @var bool\n     */\n    protected $calDAVEnabled;\n\n    /**\n     * is CardDAV enabled?\n     *\n     * @var bool\n     */\n    protected $cardDAVEnabled;\n\n    /**\n     * is WebDAV enabled?\n     *\n     * @var bool\n     */\n    protected $webDAVEnabled;\n\n    /**\n     * Are public calendars enabled?\n     *\n     * @var bool\n     */\n    protected $publicCalendarsEnabled;\n\n    /**\n     * Mail address to send mails from.\n     *\n     * @var string\n     */\n    protected $inviteAddress;\n\n    /**\n     * Public directory of the Symfony installation.\n     * Needed to retrieve assets (images).\n     *\n     * @var string\n     */\n    protected $publicDir;\n\n    /**\n     * WebDAV Public directory.\n     *\n     * @var string\n     */\n    protected $webdavPublicDir;\n\n    /**\n     * WebDAV User Homes directory.\n     *\n     * @var string|null\n     */\n    protected $webdavHomesDir;\n\n    /**\n     * WebDAV Temporary directory.\n     *\n     * @var string\n     */\n    protected $webdavTmpDir;\n\n    /**\n     * @var EntityManagerInterface\n     */\n    protected $em;\n\n    /**\n     * @var MailerInterface\n     */\n    protected $mailer;\n\n    /**\n     * @var BirthdayService\n     */\n    protected $birthdayService;\n\n    /**\n     * Base URI of the server.\n     *\n     * @var string\n     */\n    protected $baseUri;\n\n    /**\n     * Basic Auth Backend class.\n     *\n     * @var BasicAuth\n     */\n    protected $basicAuthBackend;\n\n    /**\n     * IMAP Auth Backend class.\n     *\n     * @var IMAPAuth\n     */\n    protected $IMAPAuthBackend;\n\n    /**\n     * LDAP Auth Backend class.\n     *\n     * @var LDAPAuth\n     */\n    protected $LDAPAuthBackend;\n\n    /**\n     * Logger for exceptions.\n     *\n     * @var Psr\\Log\\LoggerInterface;\n     */\n    protected $logger;\n\n    /**\n     * Server.\n     *\n     * @var \\Sabre\\DAV\\Server\n     */\n    protected $server;\n\n    public function __construct(MailerInterface $mailer, BasicAuth $basicAuthBackend, IMAPAuth $IMAPAuthBackend, LDAPAuth $LDAPAuthBackend, UrlGeneratorInterface $router, EntityManagerInterface $entityManager, LoggerInterface $logger, BirthdayService $birthdayService, string $publicDir, bool $calDAVEnabled = true, bool $cardDAVEnabled = true, bool $webDAVEnabled = false, bool $publicCalendarsEnabled = true, ?string $inviteAddress = null, ?string $authMethod = null, ?string $authRealm = null, ?string $webdavPublicDir = null, ?string $webdavHomesDir = null, ?string $webdavTmpDir = null)\n    {\n        $this->publicDir = $publicDir;\n\n        $this->calDAVEnabled = $calDAVEnabled;\n        $this->cardDAVEnabled = $cardDAVEnabled;\n        $this->webDAVEnabled = $webDAVEnabled;\n        $this->publicCalendarsEnabled = $publicCalendarsEnabled;\n        $this->inviteAddress = $inviteAddress ?? null;\n\n        $this->webdavPublicDir = $webdavPublicDir;\n        $this->webdavHomesDir = $webdavHomesDir;\n        $this->webdavTmpDir = $webdavTmpDir;\n\n        $this->em = $entityManager;\n        $this->logger = $logger;\n        $this->mailer = $mailer;\n        $this->birthdayService = $birthdayService;\n        $this->baseUri = $router->generate('dav', ['path' => '']);\n\n        $this->basicAuthBackend = $basicAuthBackend;\n        $this->IMAPAuthBackend = $IMAPAuthBackend;\n        $this->LDAPAuthBackend = $LDAPAuthBackend;\n\n        $this->initServer($authMethod, $authRealm);\n        $this->initExceptionListener();\n    }\n\n    #[Route('/', name: 'home')]\n    public function home(): Response\n    {\n        return $this->render('index.html.twig', [\n            'version' => \\App\\Version::VERSION,\n        ]);\n    }\n\n    private function initServer(string $authMethod, string $authRealm = User::DEFAULT_AUTH_REALM)\n    {\n        // Get the PDO Connection of type PDO\n        $pdo = $this->em->getConnection()->getNativeConnection();\n\n        /*\n         * The backends.\n         */\n        switch ($authMethod) {\n            case self::AUTH_IMAP:\n                $authBackend = $this->IMAPAuthBackend;\n                break;\n            case self::AUTH_LDAP:\n                $authBackend = $this->LDAPAuthBackend;\n                break;\n            case self::AUTH_BASIC:\n            default:\n                $authBackend = $this->basicAuthBackend;\n                break;\n        }\n\n        $authBackend->setRealm($authRealm);\n\n        $principalBackend = new \\Sabre\\DAVACL\\PrincipalBackend\\PDO($pdo);\n\n        /**\n         * The directory tree.\n         *\n         * Basically this is an array which contains the 'top-level' directories in the\n         * WebDAV server.\n         */\n        $nodes = [\n            // /principals\n            new \\Sabre\\CalDAV\\Principal\\Collection($principalBackend),\n        ];\n\n        if ($this->webdavHomesDir) {\n            $nodes[] = new \\Sabre\\DAVACL\\FS\\HomeCollection($principalBackend, $this->webdavHomesDir);\n        }\n\n        if ($this->calDAVEnabled) {\n            $calendarBackend = new \\Sabre\\CalDAV\\Backend\\PDO($pdo);\n            $nodes[] = new \\Sabre\\CalDAV\\CalendarRoot($principalBackend, $calendarBackend);\n        }\n        if ($this->cardDAVEnabled) {\n            $carddavBackend = new \\Sabre\\CardDAV\\Backend\\PDO($pdo);\n            $nodes[] = new \\Sabre\\CardDAV\\AddressBookRoot($principalBackend, $carddavBackend);\n        }\n        if ($this->webDAVEnabled && $this->webdavTmpDir && $this->webdavPublicDir) {\n            $nodes[] = new \\Sabre\\DAV\\FS\\Directory($this->webdavPublicDir);\n        }\n\n        // The object tree needs in turn to be passed to the server class\n        $this->server = new \\Sabre\\DAV\\Server($nodes);\n        $this->server->setBaseUri($this->baseUri);\n\n        // Plugins\n        $this->server->addPlugin(new \\Sabre\\DAV\\Auth\\Plugin($authBackend, $authRealm));\n        $this->server->addPlugin(new \\Sabre\\DAV\\Browser\\Plugin(false)); // We disable the file creation / upload / sharing in the browser\n        $this->server->addPlugin(new \\Sabre\\DAV\\Sync\\Plugin());\n\n        $aclPlugin = new PublicAwareDAVACLPlugin($this->em, $this->publicCalendarsEnabled);\n        $aclPlugin->hideNodesFromListings = true;\n        $aclPlugin->allowUnauthenticatedAccess = true; // Already the default, but setting it is future-proof\n\n        // Fetch admins, if any\n        $admins = $this->em->getRepository(Principal::class)->findBy(['isAdmin' => true]);\n        foreach ($admins as $principal) {\n            $aclPlugin->adminPrincipals[] = $principal->getUri();\n        }\n\n        $this->server->addPlugin($aclPlugin);\n\n        $this->server->addPlugin(new \\Sabre\\DAV\\PropertyStorage\\Plugin(\n            new \\Sabre\\DAV\\PropertyStorage\\Backend\\PDO($pdo)\n        ));\n\n        // CalDAV plugins\n        if ($this->calDAVEnabled) {\n            $this->server->addPlugin(new \\Sabre\\DAV\\Sharing\\Plugin());\n            $this->server->addPlugin(new \\Sabre\\CalDAV\\Plugin());\n            $this->server->addPlugin(new \\Sabre\\CalDAV\\Schedule\\Plugin());\n            $this->server->addPlugin(new \\Sabre\\CalDAV\\SharingPlugin());\n            $this->server->addPlugin(new \\Sabre\\CalDAV\\ICSExportPlugin());\n            $this->server->addPlugin(new \\Sabre\\CalDAV\\Subscriptions\\Plugin());\n            if ($this->inviteAddress) {\n                $this->server->addPlugin(new DavisIMipPlugin($this->mailer, $this->inviteAddress, $this->publicDir));\n            }\n        }\n\n        // CardDAV plugins\n        if ($this->cardDAVEnabled) {\n            $this->server->addPlugin(new \\Sabre\\CardDAV\\Plugin());\n            $this->server->addPlugin(new \\Sabre\\CardDAV\\VCFExportPlugin());\n        }\n\n        if ($this->cardDAVEnabled && $this->calDAVEnabled) {\n            $this->server->addPlugin(new BirthdayCalendarPlugin($this->birthdayService, $calendarBackend));\n        }\n\n        // WebDAV plugins\n        if ($this->webDAVEnabled && $this->webdavTmpDir && $this->webdavPublicDir) {\n            if (!is_dir($this->webdavTmpDir) || !is_dir($this->webdavPublicDir)) {\n                throw new \\Exception('The WebDAV temp dir and/or public dir are not available. Make sure they are created with the correct permissions.');\n            }\n            $lockBackend = new \\Sabre\\DAV\\Locks\\Backend\\File($this->webdavTmpDir.'/locksdb');\n            $this->server->addPlugin(new \\Sabre\\DAV\\Locks\\Plugin($lockBackend));\n            $this->server->addPlugin(new \\Sabre\\DAV\\Browser\\GuessContentType());\n            $this->server->addPlugin(new \\Sabre\\DAV\\TemporaryFileFilterPlugin($this->webdavTmpDir));\n        }\n    }\n\n    private function initExceptionListener()\n    {\n        $this->server->on('exception', function (\\Throwable $e) {\n            // We don't need a trace for simple authentication exceptions\n            if ($e instanceof \\Sabre\\DAV\\Exception\\NotAuthenticated) {\n                $this->logger->warning('[401]: '.get_class($e).\" - No 'Authorization: Basic' header found. Login was needed\");\n\n                return;\n            }\n\n            $httpCode = ($e instanceof \\Sabre\\DAV\\Exception) ? $e->getHTTPCode() : 500;\n            $this->logger->error('['.$httpCode.']: '.get_class($e).' - '.$e->getMessage(), $e->getTrace());\n        });\n    }\n\n    #[Route('/dav/{path}', name: 'dav', requirements: ['path' => '.*'])]\n    public function dav(Request $request, ?string $path, ?Profiler $profiler = null)\n    {\n        // We don't want the toolbar on the /dav/* routes\n        if ($profiler instanceof Profiler) {\n            $profiler->disable();\n        }\n\n        // We need to acknowledge the OPTIONS call before sabre/dav for public\n        // calendars since we're circumventing the lib\n        if ('OPTIONS' === $request->getMethod()) {\n            $response = new Response();\n\n            // Adapted from CorePlugin's httpOptions()\n            // https://github.com/sabre-io/dav/blob/master/lib/DAV/CorePlugin.php#L210\n            $methods = $this->server->getAllowedMethods('');\n\n            $response->headers->set('Allow', strtoupper(implode(', ', $methods)));\n            $features = ['1', '3', 'extended-mkcol'];\n\n            foreach ($this->server->getPlugins() as $plugin) {\n                $features = array_merge($features, $plugin->getFeatures());\n            }\n\n            $response->headers->set('DAV', implode(', ', $features));\n            $response->headers->set('MS-Author-Via', 'DAV');\n\n            return $response;\n        }\n\n        // \\Sabre\\DAV\\Server does not let us use a custom SAPI, and its behaviour\n        // is to directly output headers and content to php://output. Hence, we\n        // let the headers pass (we have not choice) and capture the output in a\n        // buffer.\n        // This allows us to use a Response, and not to break the events triggered\n        // by Symfony after the response is sent, like for instance the TERMINATE\n        // event from the Kernel, that is used to send emails...\n\n        ob_start(); // Does not capture headers!\n        $this->server->start();\n\n        $output = ob_get_contents();\n        ob_end_clean();\n\n        // As previously said, headers are already _prepared_ by the server,\n        // so we can't modify them or remove them. But they are not _sent_ yet,\n        // so headers_sent() is false, and Symfony will add its own headers above it.\n        //\n        // The Content-type header is the problem, since Symfony will\n        // output `text/html` for everything since it doesn't know any better.\n        // Thus, we have to get the _real_ Content-type header already prepared,\n        // and force it in the Symfony Response.\n        //\n        // That's what we do here.\n        $response = new Response($output, http_response_code(), []);\n        foreach (headers_list() as $header) {\n            if ('content-type:' === strtolower(substr($header, 0, 13))) {\n                $headerArray = explode(':', $header);\n                $response->headers->set('Content-type', $headerArray[1]);\n            }\n        }\n\n        return $response;\n    }\n}\n"
  },
  {
    "path": "src/Controller/SecurityController.php",
    "content": "<?php\n\nnamespace App\\Controller;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationUtils;\n\nclass SecurityController extends AbstractController\n{\n    #[Route('/login', name: 'app_login')]\n    public function login(AuthenticationUtils $authenticationUtils): Response\n    {\n        // if ($this->getUser()) {\n        //     return $this->redirectToRoute('target_path');\n        // }\n\n        // get the login error if there is one\n        $error = $authenticationUtils->getLastAuthenticationError();\n        // last username entered by the user\n        $lastUsername = $authenticationUtils->getLastUsername();\n\n        return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);\n    }\n\n    #[Route('/logout', name: 'app_logout')]\n    public function logout()\n    {\n        throw new \\Exception('This method can be blank - it will be intercepted by the logout key on your firewall');\n    }\n}\n"
  },
  {
    "path": "src/DataFixtures/AppFixtures.php",
    "content": "<?php\n\nnamespace App\\DataFixtures;\n\nuse App\\Entity\\AddressBook;\nuse App\\Entity\\Calendar;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\Principal;\nuse App\\Entity\\User;\nuse Doctrine\\Bundle\\FixturesBundle\\Fixture;\nuse Doctrine\\Persistence\\ObjectManager;\n\nclass AppFixtures extends Fixture\n{\n    public function load(ObjectManager $manager): void\n    {\n        // Test User 1\n        $hash = password_hash('password', PASSWORD_DEFAULT);\n        $user = (new User())\n            ->setUsername('test_user')\n            ->setPassword($hash);\n        $manager->persist($user);\n\n        $principal = (new Principal())\n            ->setUri(Principal::PREFIX.$user->getUsername())\n            ->setEmail('test@test.com')\n            ->setDisplayName('Test User')\n            ->setIsAdmin(true);\n        $manager->persist($principal);\n\n        // Create all the default calendar / addressbook\n        $calendarInstance = new CalendarInstance();\n        $calendar = new Calendar();\n        $calendarInstance->setPrincipalUri(Principal::PREFIX.$user->getUsername())\n                    ->setUri('default')\n                    ->setDisplayName('default.calendar.title')\n                    ->setDescription('default.calendar.description')\n                    ->setCalendar($calendar);\n        $manager->persist($calendarInstance);\n\n        // Enable delegation by default\n        $principalProxyRead = new Principal();\n        $principalProxyRead->setUri($principal->getUri().Principal::READ_PROXY_SUFFIX)\n                            ->setIsMain(false);\n        $manager->persist($principalProxyRead);\n\n        $principalProxyWrite = new Principal();\n        $principalProxyWrite->setUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX)\n                            ->setIsMain(false);\n        $manager->persist($principalProxyWrite);\n\n        $addressbook = new AddressBook();\n        $addressbook->setPrincipalUri(Principal::PREFIX.$user->getUsername())\n                    ->setUri('default')\n                    ->setDisplayName('default.addressbook.title')\n                    ->setDescription('default.addressbook.description');\n        $manager->persist($addressbook);\n\n        $manager->flush();\n\n        // Test User 2 - For API testing\n        $hash = password_hash('password2', PASSWORD_DEFAULT);\n        $user = (new User())\n            ->setUsername('test_user2')\n            ->setPassword($hash);\n        $manager->persist($user);\n\n        $principal = (new Principal())\n            ->setUri(Principal::PREFIX.$user->getUsername())\n            ->setEmail('test2@test.com')\n            ->setDisplayName('Test User 2')\n            ->setIsAdmin(false);\n        $manager->persist($principal);\n\n        // Create all the default calendar / addressbook\n        $calendarInstance = new CalendarInstance();\n        $calendar = new Calendar();\n        $calendarInstance->setPrincipalUri(Principal::PREFIX.$user->getUsername())\n                    ->setUri('default')\n                    ->setDisplayName('default.calendar.title2')\n                    ->setDescription('default.calendar.description2')\n                    ->setCalendar($calendar);\n        $manager->persist($calendarInstance);\n\n        // Enable delegation by default\n        $principalProxyRead = new Principal();\n        $principalProxyRead->setUri($principal->getUri().Principal::READ_PROXY_SUFFIX)\n                            ->setIsMain(false);\n        $manager->persist($principalProxyRead);\n\n        $principalProxyWrite = new Principal();\n        $principalProxyWrite->setUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX)\n                            ->setIsMain(false);\n        $manager->persist($principalProxyWrite);\n\n        $addressbook = new AddressBook();\n        $addressbook->setPrincipalUri(Principal::PREFIX.$user->getUsername())\n                    ->setUri('default')\n                    ->setDisplayName('default.addressbook.title2')\n                    ->setDescription('default.addressbook.description2');\n        $manager->persist($addressbook);\n\n        $manager->flush();\n    }\n}\n"
  },
  {
    "path": "src/Entity/AddressBook.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\Common\\Collections\\ArrayCollection;\nuse Doctrine\\Common\\Collections\\Collection;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntity;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'addressbooks')]\n#[UniqueEntity(fields: ['principalUri', 'uri'], errorPath: 'uri', message: 'form.uri.unique')]\nclass AddressBook\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(name: 'principaluri', type: 'string', length: 255)]\n    private $principalUri;\n\n    #[ORM\\Column(name: 'displayname', type: 'string', length: 255)]\n    private $displayName;\n\n    #[ORM\\Column(type: 'string', length: 255)]\n    #[Assert\\Regex(\"/[0-9a-z\\-]+/\")]\n    private $uri;\n\n    #[ORM\\Column(type: 'text', nullable: true)]\n    private $description;\n\n    #[ORM\\Column(type: 'string', length: 255)]\n    private $synctoken;\n\n    #[ORM\\Column(type: 'boolean', nullable: true, options: ['default' => false])]\n    private $includedInBirthdayCalendar;\n\n    #[ORM\\OneToMany(targetEntity: \"App\\Entity\\Card\", mappedBy: 'addressBook')]\n    private $cards;\n\n    #[ORM\\OneToMany(targetEntity: \"App\\Entity\\AddressBookChange\", mappedBy: 'addressBook')]\n    private $changes;\n\n    public function __construct()\n    {\n        $this->synctoken = 1;\n        $this->includedInBirthdayCalendar = false;\n        $this->cards = new ArrayCollection();\n        $this->changes = new ArrayCollection();\n    }\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getPrincipalUri(): ?string\n    {\n        return $this->principalUri;\n    }\n\n    public function setPrincipalUri(string $principalUri): self\n    {\n        $this->principalUri = $principalUri;\n\n        return $this;\n    }\n\n    public function getDisplayName(): ?string\n    {\n        return $this->displayName;\n    }\n\n    public function setDisplayName(string $displayName): self\n    {\n        $this->displayName = $displayName;\n\n        return $this;\n    }\n\n    public function isIncludedInBirthdayCalendar(): ?bool\n    {\n        return $this->includedInBirthdayCalendar;\n    }\n\n    public function setIncludedInBirthdayCalendar(bool $includedInBirthdayCalendar): self\n    {\n        $this->includedInBirthdayCalendar = $includedInBirthdayCalendar;\n\n        return $this;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n\n    public function getDescription(): ?string\n    {\n        return $this->description;\n    }\n\n    public function setDescription(string $description): self\n    {\n        $this->description = $description;\n\n        return $this;\n    }\n\n    public function getSynctoken(): ?string\n    {\n        return $this->synctoken;\n    }\n\n    public function setSynctoken(string $synctoken): self\n    {\n        $this->synctoken = $synctoken;\n\n        return $this;\n    }\n\n    /**\n     * @return Collection|Card[]\n     */\n    public function getCards(): Collection\n    {\n        return $this->cards;\n    }\n\n    public function addCard(Card $card): self\n    {\n        if (!$this->cards->contains($card)) {\n            $this->cards[] = $card;\n            $card->setAddressBook($this);\n        }\n\n        return $this;\n    }\n\n    public function removeCard(Card $card): self\n    {\n        if ($this->cards->contains($card)) {\n            $this->cards->removeElement($card);\n            // set the owning side to null (unless already changed)\n            if ($card->getAddressBook() === $this) {\n                $card->setAddressBook(null);\n            }\n        }\n\n        return $this;\n    }\n\n    /**\n     * @return Collection|AddressBookChange[]\n     */\n    public function getChanges(): Collection\n    {\n        return $this->changes;\n    }\n\n    public function addChange(AddressBookChange $change): self\n    {\n        if (!$this->changes->contains($change)) {\n            $this->changes[] = $change;\n            $change->setCalendar($this);\n        }\n\n        return $this;\n    }\n\n    public function removeChange(AddressBookChange $change): self\n    {\n        if ($this->changes->contains($change)) {\n            $this->changes->removeElement($change);\n            // set the owning side to null (unless already changed)\n            if ($change->getCalendar() === $this) {\n                $change->setCalendar(null);\n            }\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/AddressBookChange.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'addressbookchanges')]\nclass AddressBookChange\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(type: 'string', length: 255)]\n    private $uri;\n\n    #[ORM\\Column(type: 'string', length: 255)]\n    private $synctoken;\n\n    #[ORM\\ManyToOne(targetEntity: \"App\\Entity\\AddressBook\", inversedBy: 'changes')]\n    #[ORM\\JoinColumn(name: 'addressbookid', nullable: false)]\n    private $addressBook;\n\n    #[ORM\\Column(type: 'integer')]\n    private $operation;\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n\n    public function getSynctoken(): ?string\n    {\n        return $this->synctoken;\n    }\n\n    public function setSynctoken(string $synctoken): self\n    {\n        $this->synctoken = $synctoken;\n\n        return $this;\n    }\n\n    public function getAddressBook(): ?AddressBook\n    {\n        return $this->addressBook;\n    }\n\n    public function setAddressBook(?AddressBook $addressBook): self\n    {\n        $this->addressBook = $addressBook;\n\n        return $this;\n    }\n\n    public function getOperation(): ?int\n    {\n        return $this->operation;\n    }\n\n    public function setOperation(int $operation): self\n    {\n        $this->operation = $operation;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/Calendar.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\Common\\Collections\\ArrayCollection;\nuse Doctrine\\Common\\Collections\\Collection;\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'calendars')]\nclass Calendar\n{\n    public const COMPONENT_EVENTS = 'VEVENT';\n    public const COMPONENT_TODOS = 'VTODO';\n    public const COMPONENT_NOTES = 'VJOURNAL';\n\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(type: 'string', length: 255)]\n    private $synctoken;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    private $components;\n\n    #[ORM\\OneToMany(targetEntity: \"App\\Entity\\CalendarObject\", mappedBy: 'calendar')]\n    private $objects;\n\n    #[ORM\\OneToMany(targetEntity: \"App\\Entity\\CalendarChange\", mappedBy: 'calendar')]\n    private $changes;\n\n    #[ORM\\OneToMany(targetEntity: \"App\\Entity\\CalendarInstance\", mappedBy: 'calendar')]\n    private $instances;\n\n    public function __construct()\n    {\n        $this->synctoken = 1;\n        $this->components = static::COMPONENT_EVENTS;\n        $this->objects = new ArrayCollection();\n        $this->changes = new ArrayCollection();\n    }\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getSynctoken(): ?string\n    {\n        return $this->synctoken;\n    }\n\n    public function setSynctoken(string $synctoken): self\n    {\n        $this->synctoken = $synctoken;\n\n        return $this;\n    }\n\n    public function getComponents(): ?string\n    {\n        return $this->components;\n    }\n\n    public function setComponents(?string $components): self\n    {\n        $this->components = $components;\n\n        return $this;\n    }\n\n    /**\n     * @return Collection|CalendarObject[]\n     */\n    public function getObjects(): Collection\n    {\n        return $this->objects;\n    }\n\n    public function addObject(CalendarObject $object): self\n    {\n        if (!$this->objects->contains($object)) {\n            $this->objects[] = $object;\n            $object->setCalendar($this);\n        }\n\n        return $this;\n    }\n\n    public function removeObject(CalendarObject $object): self\n    {\n        if ($this->objects->contains($object)) {\n            $this->objects->removeElement($object);\n            // set the owning side to null (unless already changed)\n            if ($object->getCalendar() === $this) {\n                $object->setCalendar(null);\n            }\n        }\n\n        return $this;\n    }\n\n    /**\n     * @return Collection|CalendarChange[]\n     */\n    public function getChanges(): Collection\n    {\n        return $this->changes;\n    }\n\n    public function addChange(CalendarChange $change): self\n    {\n        if (!$this->changes->contains($change)) {\n            $this->changes[] = $change;\n            $change->setCalendar($this);\n        }\n\n        return $this;\n    }\n\n    public function removeChange(CalendarChange $change): self\n    {\n        if ($this->changes->contains($change)) {\n            $this->changes->removeElement($change);\n            // set the owning side to null (unless already changed)\n            if ($change->getCalendar() === $this) {\n                $change->setCalendar(null);\n            }\n        }\n\n        return $this;\n    }\n\n    /**\n     * @return Collection|CalendarInstance[]\n     */\n    public function getInstances(): Collection\n    {\n        return $this->instances;\n    }\n\n    /**\n     * Check if this calendar supports a specific component type.\n     *\n     * @param string $componentType The component type to check\n     *\n     * @return bool True if the component is supported, false otherwise\n     */\n    public function isComponentEnabled(string $componentType): bool\n    {\n        return in_array($componentType, explode(',', $this->components ?? ''), true);\n    }\n}\n"
  },
  {
    "path": "src/Entity/CalendarChange.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'calendarchanges')]\nclass CalendarChange\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(type: 'string', length: 255)]\n    private $uri;\n\n    #[ORM\\Column(type: 'integer')]\n    private $synctoken;\n\n    #[ORM\\ManyToOne(targetEntity: \"App\\Entity\\Calendar\", inversedBy: 'changes')]\n    #[ORM\\JoinColumn(name: 'calendarid', nullable: false)]\n    private $calendar;\n\n    #[ORM\\Column(type: 'smallint')]\n    private $operation;  // 1 = create, 2 = update, 3 = delete\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n\n    public function getSynctoken(): ?int\n    {\n        return $this->synctoken;\n    }\n\n    public function setSynctoken(int $synctoken): self\n    {\n        $this->synctoken = $synctoken;\n\n        return $this;\n    }\n\n    public function getCalendar(): ?Calendar\n    {\n        return $this->calendar;\n    }\n\n    public function setCalendar(?Calendar $calendar): self\n    {\n        $this->calendar = $calendar;\n\n        return $this;\n    }\n\n    public function getOperation(): ?int\n    {\n        return $this->operation;\n    }\n\n    public function setOperation(int $operation): self\n    {\n        $this->operation = $operation;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/CalendarInstance.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse App\\Constants;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Sabre\\DAV\\Sharing\\Plugin as SharingPlugin;\nuse Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntity;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n#[ORM\\Entity(repositoryClass: \"App\\Repository\\CalendarInstanceRepository\")]\n#[ORM\\Table(name: 'calendarinstances')]\n#[UniqueEntity(fields: ['principalUri', 'uri'], errorPath: 'uri', message: 'form.uri.unique')]\nclass CalendarInstance\n{\n    public static function getOwnerAccesses(): array\n    {\n        return [\n            SharingPlugin::ACCESS_NOTSHARED,\n            SharingPlugin::ACCESS_SHAREDOWNER,\n        ];\n    }\n\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\ManyToOne(targetEntity: \"App\\Entity\\Calendar\", cascade: ['persist'], inversedBy: 'instances')]\n    #[ORM\\JoinColumn(name: 'calendarid', nullable: false)]\n    private $calendar;\n\n    #[ORM\\Column(name: 'principaluri', type: 'string', length: 255, nullable: true)]\n    private $principalUri;\n\n    #[ORM\\Column(type: 'smallint', options: ['default' => 1])]\n    private $access;\n\n    #[ORM\\Column(name: 'displayname', type: 'string', length: 255, nullable: true)]\n    private $displayName;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    #[Assert\\Regex(\"/[0-9a-z\\-]+/\")]\n    private $uri;\n\n    #[ORM\\Column(type: 'text', nullable: true)]\n    private $description;\n\n    #[ORM\\Column(name: 'calendarorder', type: 'integer', options: ['default' => 0])]\n    private $calendarOrder;\n\n    #[ORM\\Column(name: 'calendarcolor', type: 'string', length: 10, nullable: true)]\n    #[Assert\\Regex(\"/\\#[0-9A-F]{6}/\")]\n    private $calendarColor;\n\n    #[ORM\\Column(type: 'text', nullable: true)]\n    private $timezone;\n\n    #[ORM\\Column(type: 'integer', nullable: true)]\n    private $transparent;\n\n    #[ORM\\Column(name: 'share_href', type: 'string', length: 255, nullable: true)]\n    private $shareHref;\n\n    #[ORM\\Column(name: 'share_displayname', type: 'string', length: 255, nullable: true)]\n    private $shareDisplayName;\n\n    #[ORM\\Column(name: 'share_invitestatus', type: 'integer', options: ['default' => 2])]\n    private $shareInviteStatus;\n\n    #[ORM\\Column(name: 'public', type: 'boolean', options: ['default' => false])]\n    private $public;\n\n    public function __construct()\n    {\n        $this->shareInviteStatus = SharingPlugin::INVITE_ACCEPTED;\n        $this->transparent = 0;\n        $this->calendarOrder = 0;\n        $this->access = SharingPlugin::ACCESS_SHAREDOWNER;\n        $this->public = false;\n    }\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getCalendar(): ?Calendar\n    {\n        return $this->calendar;\n    }\n\n    public function setCalendar(?Calendar $calendar): self\n    {\n        $this->calendar = $calendar;\n\n        return $this;\n    }\n\n    public function getPrincipalUri(): ?string\n    {\n        return $this->principalUri;\n    }\n\n    public function setPrincipalUri(?string $principalUri): self\n    {\n        $this->principalUri = $principalUri;\n\n        return $this;\n    }\n\n    public function getAccess(): ?int\n    {\n        return $this->access;\n    }\n\n    public function setAccess(int $access): self\n    {\n        $this->access = $access;\n\n        return $this;\n    }\n\n    public function isShared(): bool\n    {\n        return !in_array($this->access, self::getOwnerAccesses());\n    }\n\n    public function setPublic(bool $public): self\n    {\n        $this->public = $public;\n\n        return $this;\n    }\n\n    public function isPublic(): bool\n    {\n        return $this->public;\n    }\n\n    public function isAutomaticallyGenerated(): bool\n    {\n        return in_array($this->uri, [Constants::BIRTHDAY_CALENDAR_URI]);\n    }\n\n    public function getDisplayName(): ?string\n    {\n        return $this->displayName;\n    }\n\n    public function setDisplayName(?string $displayName): self\n    {\n        $this->displayName = $displayName;\n\n        return $this;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(?string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n\n    public function getDescription(): ?string\n    {\n        return $this->description;\n    }\n\n    public function setDescription(?string $description): self\n    {\n        $this->description = $description;\n\n        return $this;\n    }\n\n    public function getCalendarOrder(): ?int\n    {\n        return $this->calendarOrder;\n    }\n\n    public function setCalendarOrder(int $calendarOrder): self\n    {\n        $this->calendarOrder = $calendarOrder;\n\n        return $this;\n    }\n\n    public function getCalendarColor(): ?string\n    {\n        return $this->calendarColor;\n    }\n\n    public function setCalendarColor(?string $calendarColor): self\n    {\n        $this->calendarColor = $calendarColor;\n\n        return $this;\n    }\n\n    public function getTimezone(): ?string\n    {\n        return $this->timezone;\n    }\n\n    public function setTimezone(?string $timezone): self\n    {\n        $this->timezone = $timezone;\n\n        return $this;\n    }\n\n    public function getTransparent(): ?int\n    {\n        return $this->transparent;\n    }\n\n    public function setTransparent(?int $transparent): self\n    {\n        $this->transparent = $transparent;\n\n        return $this;\n    }\n\n    public function getShareHref(): ?string\n    {\n        return $this->shareHref;\n    }\n\n    public function setShareHref(?string $shareHref): self\n    {\n        $this->shareHref = $shareHref;\n\n        return $this;\n    }\n\n    public function getShareDisplayName(): ?string\n    {\n        return $this->shareDisplayName;\n    }\n\n    public function setShareDisplayName(?string $shareDisplayName): self\n    {\n        $this->shareDisplayName = $shareDisplayName;\n\n        return $this;\n    }\n\n    public function getShareInviteStatus(): ?int\n    {\n        return $this->shareInviteStatus;\n    }\n\n    public function setShareInviteStatus(int $shareInviteStatus): self\n    {\n        $this->shareInviteStatus = $shareInviteStatus;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/CalendarObject.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'calendarobjects')]\nclass CalendarObject\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    /**\n     * The length corresponds to MEDIUMTEXT in MySQL.\n     */\n    #[ORM\\Column(name: 'calendardata', type: 'text', length: 16777215, nullable: true)]\n    private $calendarData;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    private $uri;\n\n    #[ORM\\ManyToOne(targetEntity: \"App\\Entity\\Calendar\", inversedBy: 'objects')]\n    #[ORM\\JoinColumn(name: 'calendarid', nullable: false)]\n    private $calendar;\n\n    #[ORM\\Column(name: 'lastmodified', type: 'bigint', nullable: true)]\n    private $lastModified;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    private $etag;\n\n    #[ORM\\Column(type: 'integer')]\n    private $size;\n\n    #[ORM\\Column(name: 'componenttype', type: 'string', length: 255, nullable: true)]\n    private $componentType;\n\n    #[ORM\\Column(name: 'firstoccurence', type: 'bigint', nullable: true)]\n    private $firstOccurence;\n\n    #[ORM\\Column(name: 'lastoccurence', type: 'bigint', nullable: true)]\n    private $lastOccurence;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    private $uid;\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getCalendarData(): ?string\n    {\n        return $this->calendarData;\n    }\n\n    public function setCalendarData(?string $calendarData): self\n    {\n        $this->calendarData = $calendarData;\n\n        return $this;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(?string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n\n    public function getCalendar(): ?Calendar\n    {\n        return $this->calendar;\n    }\n\n    public function setCalendar(?Calendar $calendar): self\n    {\n        $this->calendar = $calendar;\n\n        return $this;\n    }\n\n    public function getLastModifier(): ?int\n    {\n        return $this->lastModifier;\n    }\n\n    public function setLastModifier(?int $lastModifier): self\n    {\n        $this->lastModifier = $lastModifier;\n\n        return $this;\n    }\n\n    public function getEtag(): ?string\n    {\n        return $this->etag;\n    }\n\n    public function setEtag(?string $etag): self\n    {\n        $this->etag = $etag;\n\n        return $this;\n    }\n\n    public function getSize(): ?int\n    {\n        return $this->size;\n    }\n\n    public function setSize(int $size): self\n    {\n        $this->size = $size;\n\n        return $this;\n    }\n\n    public function getComponentType(): ?string\n    {\n        return $this->componentType;\n    }\n\n    public function setComponentType(?string $componentType): self\n    {\n        $this->componentType = $componentType;\n\n        return $this;\n    }\n\n    public function getFirstOccurence(): ?int\n    {\n        return $this->firstOccurence;\n    }\n\n    public function setFirstOccurence(?int $firstOccurence): self\n    {\n        $this->firstOccurence = $firstOccurence;\n\n        return $this;\n    }\n\n    public function getLastOccurence(): ?int\n    {\n        return $this->lastOccurence;\n    }\n\n    public function setLastOccurence(?int $lastOccurence): self\n    {\n        $this->lastOccurence = $lastOccurence;\n\n        return $this;\n    }\n\n    public function getUid(): ?string\n    {\n        return $this->uid;\n    }\n\n    public function setUid(?string $uid): self\n    {\n        $this->uid = $uid;\n\n        return $this;\n    }\n\n    public function getLastModified(): ?int\n    {\n        return $this->lastModified;\n    }\n\n    public function setLastModified(?int $lastModified): self\n    {\n        $this->lastModified = $lastModified;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/CalendarSubscription.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'calendarsubscriptions')]\nclass CalendarSubscription\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(type: 'string', length: 255)]\n    private $uri;\n\n    #[ORM\\Column(name: 'principaluri', type: 'string', length: 255)]\n    private $principalUri;\n\n    #[ORM\\Column(type: 'text', nullable: true)]\n    private $source;\n\n    #[ORM\\Column(name: 'displayname', type: 'string', length: 255, nullable: true)]\n    private $displayName;\n\n    #[ORM\\Column(name: 'refreshrate', type: 'string', length: 10, nullable: true)]\n    private $refreshRate;\n\n    #[ORM\\Column(name: 'calendarorder', type: 'integer')]\n    private $calendarOrder;\n\n    #[ORM\\Column(name: 'calendarcolor', type: 'string', length: 10, nullable: true)]\n    private $calendarColor;\n\n    #[ORM\\Column(name: 'striptodos', type: 'smallint', nullable: true)]\n    private $stripTodos;\n\n    #[ORM\\Column(name: 'stripalarms', type: 'smallint', nullable: true)]\n    private $stripAlarms;\n\n    #[ORM\\Column(name: 'stripattachments', type: 'smallint', nullable: true)]\n    private $stripAttachments;\n\n    #[ORM\\Column(name: 'lastmodified', type: 'bigint', nullable: true)]\n    private $lastModified;\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n\n    public function getPrincipalUri(): ?string\n    {\n        return $this->principalUri;\n    }\n\n    public function setPrincipalUri(string $principalUri): self\n    {\n        $this->principalUri = $principalUri;\n\n        return $this;\n    }\n\n    public function getSource(): ?string\n    {\n        return $this->source;\n    }\n\n    public function setSource(?string $source): self\n    {\n        $this->source = $source;\n\n        return $this;\n    }\n\n    public function getDisplayName(): ?string\n    {\n        return $this->displayName;\n    }\n\n    public function setDisplayName(?string $displayName): self\n    {\n        $this->displayName = $displayName;\n\n        return $this;\n    }\n\n    public function getRefreshRate(): ?string\n    {\n        return $this->refreshRate;\n    }\n\n    public function setRefreshRate(?string $refreshRate): self\n    {\n        $this->refreshRate = $refreshRate;\n\n        return $this;\n    }\n\n    public function getCalendarOrder(): ?int\n    {\n        return $this->calendarOrder;\n    }\n\n    public function setCalendarOrder(int $calendarOrder): self\n    {\n        $this->calendarOrder = $calendarOrder;\n\n        return $this;\n    }\n\n    public function getCalendarColor(): ?string\n    {\n        return $this->calendarColor;\n    }\n\n    public function setCalendarColor(?string $calendarColor): self\n    {\n        $this->calendarColor = $calendarColor;\n\n        return $this;\n    }\n\n    public function getStripTodos(): ?int\n    {\n        return $this->stripTodos;\n    }\n\n    public function setStripTodos(?int $stripTodos): self\n    {\n        $this->stripTodos = $stripTodos;\n\n        return $this;\n    }\n\n    public function getStripAlarms(): ?int\n    {\n        return $this->stripAlarms;\n    }\n\n    public function setStripAlarms(?int $stripAlarms): self\n    {\n        $this->stripAlarms = $stripAlarms;\n\n        return $this;\n    }\n\n    public function getStripAttachments(): ?int\n    {\n        return $this->stripAttachments;\n    }\n\n    public function setStripAttachments(?int $stripAttachments): self\n    {\n        $this->stripAttachments = $stripAttachments;\n\n        return $this;\n    }\n\n    public function getLastModified(): ?int\n    {\n        return $this->lastModified;\n    }\n\n    public function setLastModified(?int $lastModified): self\n    {\n        $this->lastModified = $lastModified;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/Card.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'cards')]\nclass Card\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\ManyToOne(targetEntity: \"App\\Entity\\AddressBook\", inversedBy: 'cards')]\n    #[ORM\\JoinColumn(name: 'addressbookid', nullable: false)]\n    private $addressBook;\n\n    /**\n     * The length corresponds to MEDIUMTEXT in MySQL.\n     */\n    #[ORM\\Column(name: 'carddata', type: 'text', length: 16777215, nullable: true)]\n    private $cardData;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    private $uri;\n\n    #[ORM\\Column(name: 'lastmodified', type: 'integer', nullable: true)]\n    private $lastModified;\n\n    #[ORM\\Column(type: 'string', length: 32, nullable: true)]\n    private $etag;\n\n    #[ORM\\Column(type: 'integer')]\n    private $size;\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getAddressBook(): ?AddressBook\n    {\n        return $this->addressBook;\n    }\n\n    public function setAddressBook(?AddressBook $addressBook): self\n    {\n        $this->addressBook = $addressBook;\n\n        return $this;\n    }\n\n    public function getCardData(): ?string\n    {\n        return $this->cardData;\n    }\n\n    public function setCardData(?string $cardData): self\n    {\n        $this->cardData = $cardData;\n\n        return $this;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(?string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n\n    public function getLastModified(): ?int\n    {\n        return $this->lastModified;\n    }\n\n    public function setLastModified(?int $lastModified): self\n    {\n        $this->lastModified = $lastModified;\n\n        return $this;\n    }\n\n    public function getEtag(): ?string\n    {\n        return $this->etag;\n    }\n\n    public function setEtag(?string $etag): self\n    {\n        $this->etag = $etag;\n\n        return $this;\n    }\n\n    public function getSize(): ?int\n    {\n        return $this->size;\n    }\n\n    public function setSize(int $size): self\n    {\n        $this->size = $size;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/Lock.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'locks')]\nclass Lock\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    private $owner;\n\n    #[ORM\\Column(type: 'integer', nullable: true)]\n    private $timeout;\n\n    #[ORM\\Column(type: 'bigint', nullable: true)]\n    private $created;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    private $token;\n\n    #[ORM\\Column(type: 'smallint', nullable: true)]\n    private $scope;\n\n    #[ORM\\Column(type: 'smallint', nullable: true)]\n    private $depth;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    private $uri;\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getOwner(): ?string\n    {\n        return $this->owner;\n    }\n\n    public function setOwner(?string $owner): self\n    {\n        $this->owner = $owner;\n\n        return $this;\n    }\n\n    public function getTimeout(): ?int\n    {\n        return $this->timeout;\n    }\n\n    public function setTimeout(?int $timeout): self\n    {\n        $this->timeout = $timeout;\n\n        return $this;\n    }\n\n    public function getCreated(): ?int\n    {\n        return $this->created;\n    }\n\n    public function setCreated(?int $created): self\n    {\n        $this->created = $created;\n\n        return $this;\n    }\n\n    public function getToken(): ?string\n    {\n        return $this->token;\n    }\n\n    public function setToken(?string $token): self\n    {\n        $this->token = $token;\n\n        return $this;\n    }\n\n    public function getScope(): ?int\n    {\n        return $this->scope;\n    }\n\n    public function setScope(?int $scope): self\n    {\n        $this->scope = $scope;\n\n        return $this;\n    }\n\n    public function getDepth(): ?int\n    {\n        return $this->depth;\n    }\n\n    public function setDepth(?int $depth): self\n    {\n        $this->depth = $depth;\n\n        return $this;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(?string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/Principal.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\Common\\Collections\\ArrayCollection;\nuse Doctrine\\Common\\Collections\\Collection;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntity;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n#[ORM\\Entity(repositoryClass: \"App\\Repository\\PrincipalRepository\")]\n#[ORM\\Table(name: 'principals')]\n#[UniqueEntity('uri')]\nclass Principal\n{\n    public const PREFIX = 'principals/';\n\n    public const READ_PROXY_SUFFIX = '/calendar-proxy-read';\n    public const WRITE_PROXY_SUFFIX = '/calendar-proxy-write';\n\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(type: 'string', length: 255, unique: true)]\n    #[Assert\\Unique]\n    #[Assert\\NotBlank]\n    private $uri;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    #[Assert\\NotBlank]\n    #[Assert\\Email(message: \"The email '{{ value }}' is not a valid email.\")]\n    private $email;\n\n    #[ORM\\Column(name: 'displayname', type: 'string', length: 255, nullable: true)]\n    private $displayName;\n\n    #[ORM\\Column(type: 'boolean')]\n    #[Assert\\NotBlank]\n    private $isMain;\n\n    #[ORM\\Column(type: 'boolean')]\n    #[Assert\\NotBlank]\n    private $isAdmin;\n\n    #[ORM\\ManyToMany(targetEntity: 'Principal')]\n    #[ORM\\JoinTable(name: 'groupmembers')]\n    #[ORM\\JoinColumn(name: 'principal_id', referencedColumnName: 'id')]\n    #[ORM\\InverseJoinColumn(name: 'member_id', referencedColumnName: 'id')]\n    private $delegees;\n\n    public function __construct()\n    {\n        $this->delegees = new ArrayCollection();\n        $this->isMain = true;\n        $this->isAdmin = false;\n    }\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n\n    public function getUsername(): ?string\n    {\n        return str_replace(self::PREFIX, '', $this->getUri());\n    }\n\n    public function getEmail(): ?string\n    {\n        return $this->email;\n    }\n\n    public function setEmail(?string $email): self\n    {\n        $this->email = $email;\n\n        return $this;\n    }\n\n    public function getDisplayName(): ?string\n    {\n        return $this->displayName;\n    }\n\n    public function setDisplayName(?string $displayName): self\n    {\n        $this->displayName = $displayName;\n\n        return $this;\n    }\n\n    /**\n     * @return Collection|Principal[]\n     */\n    public function getDelegees(): Collection\n    {\n        return $this->delegees;\n    }\n\n    public function addDelegee(Principal $delegee): self\n    {\n        if (!$this->delegees->contains($delegee)) {\n            $this->delegees[] = $delegee;\n        }\n\n        return $this;\n    }\n\n    public function removeDelegee(Principal $delegee): self\n    {\n        if ($this->delegees->contains($delegee)) {\n            $this->delegees->removeElement($delegee);\n        }\n\n        return $this;\n    }\n\n    public function removeAllDelegees(): self\n    {\n        $this->delegees->clear();\n\n        return $this;\n    }\n\n    public function getIsMain(): ?bool\n    {\n        return $this->isMain;\n    }\n\n    public function setIsMain(bool $isMain): self\n    {\n        $this->isMain = $isMain;\n\n        return $this;\n    }\n\n    public function getIsAdmin(): ?bool\n    {\n        return $this->isAdmin;\n    }\n\n    public function setIsAdmin(bool $isAdmin): self\n    {\n        $this->isAdmin = $isAdmin;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/PropertyStorage.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'propertystorage')]\nclass PropertyStorage\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(type: 'string', length: 255)]\n    private $path;\n\n    #[ORM\\Column(type: 'string', length: 255)]\n    private $name;\n\n    #[ORM\\Column(name: 'valuetype', type: 'integer', nullable: true)]\n    private $valueType;\n\n    #[ORM\\Column(type: 'text', nullable: true)]\n    private $value;\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getPath(): ?string\n    {\n        return $this->path;\n    }\n\n    public function setPath(string $path): self\n    {\n        $this->path = $path;\n\n        return $this;\n    }\n\n    public function getName(): ?string\n    {\n        return $this->name;\n    }\n\n    public function setName(string $name): self\n    {\n        $this->name = $name;\n\n        return $this;\n    }\n\n    public function getValueType(): ?int\n    {\n        return $this->valueType;\n    }\n\n    public function setValueType(?int $valueType): self\n    {\n        $this->valueType = $valueType;\n\n        return $this;\n    }\n\n    public function getValue(): ?string\n    {\n        return $this->value;\n    }\n\n    public function setValue(?string $value): self\n    {\n        $this->value = $value;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/SchedulingObject.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: 'schedulingobjects')]\nclass SchedulingObject\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(name: 'principaluri', type: 'string', length: 255, nullable: true)]\n    private $principalUri;\n\n    /**\n     * The length corresponds to MEDIUMTEXT in MySQL.\n     */\n    #[ORM\\Column(name: 'calendardata', type: 'text', length: 16777215, nullable: true)]\n    private $calendarData;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    #[Assert\\Regex(\"/[0-9a-z\\-]+/\")]\n    private $uri;\n\n    #[ORM\\Column(name: 'lastmodified', type: 'bigint', nullable: true)]\n    private $lastModified;\n\n    #[ORM\\Column(type: 'string', length: 255, nullable: true)]\n    private $etag;\n\n    #[ORM\\Column(type: 'integer')]\n    private $size;\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getPrincipalUri(): ?string\n    {\n        return $this->principalUri;\n    }\n\n    public function setPrincipalUri(?string $principalUri): self\n    {\n        $this->principalUri = $principalUri;\n\n        return $this;\n    }\n\n    public function getCalendarData(): ?string\n    {\n        return $this->calendarData;\n    }\n\n    public function setCalendarData(?string $calendarData): self\n    {\n        $this->calendarData = $calendarData;\n\n        return $this;\n    }\n\n    public function getUri(): ?string\n    {\n        return $this->uri;\n    }\n\n    public function setUri(?string $uri): self\n    {\n        $this->uri = $uri;\n\n        return $this;\n    }\n\n    public function getLastModified(): ?int\n    {\n        return $this->lastModified;\n    }\n\n    public function setLastModified(?int $lastModified): self\n    {\n        $this->lastModified = $lastModified;\n\n        return $this;\n    }\n\n    public function getEtag(): ?string\n    {\n        return $this->etag;\n    }\n\n    public function setEtag(?string $etag): self\n    {\n        $this->etag = $etag;\n\n        return $this;\n    }\n\n    public function getSize(): ?int\n    {\n        return $this->size;\n    }\n\n    public function setSize(int $size): self\n    {\n        $this->size = $size;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/User.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntity;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n#[ORM\\Entity()]\n#[ORM\\Table(name: '`users`')]\n#[UniqueEntity('username')]\nclass User\n{\n    public const DEFAULT_AUTH_REALM = 'SabreDAV';\n\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column(type: 'integer')]\n    private $id;\n\n    #[ORM\\Column(type: 'string', length: 255, unique: true)]\n    #[Assert\\NotBlank]\n    private $username;\n\n    #[ORM\\Column(name: 'digesta1', type: 'string', length: 255)]\n    private $password;\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getUsername(): ?string\n    {\n        return $this->username;\n    }\n\n    public function setUsername(string $username): self\n    {\n        $this->username = $username;\n\n        return $this;\n    }\n\n    public function getPassword(): ?string\n    {\n        return $this->password;\n    }\n\n    // $password _can_ be NULL here, in the case when we edit a user\n    // and do not change its password\n    public function setPassword(?string $password): self\n    {\n        $this->password = $password;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Form/AddressBookType.php",
    "content": "<?php\n\nnamespace App\\Form;\n\nuse App\\Entity\\AddressBook;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\SubmitType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextareaType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nuse Symfony\\Component\\OptionsResolver\\OptionsResolver;\n\nclass AddressBookType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options): void\n    {\n        $builder\n            ->add('principalUri', HiddenType::class, [\n                'required' => true,\n            ])\n            ->add('uri', TextType::class, [\n                'label' => 'form.uri',\n                'disabled' => !$options['new'],\n                'help' => 'form.uri.help.carddav',\n            ])\n            ->add('displayName', TextType::class, [\n                'label' => 'form.displayName',\n                'help' => 'form.name.help.carddav',\n            ])\n            ->add('includedInBirthdayCalendar', ChoiceType::class, [\n                'label' => 'form.includedInBirthdayCalendar',\n                'help' => 'form.includedInBirthdayCalendar.help',\n                'required' => true,\n                'choices' => ['yes' => true, 'no' => false],\n            ])\n            ->add('description', TextareaType::class, [\n                'label' => 'form.description',\n                'required' => false,\n            ])\n            ->add('save', SubmitType::class, [\n                'label' => 'save',\n            ]);\n\n        if (!$options['birthday_calendar_enabled']) {\n            $builder->remove('includedInBirthdayCalendar');\n        }\n    }\n\n    public function configureOptions(OptionsResolver $resolver): void\n    {\n        $resolver->setDefaults([\n            'new' => false,\n            'data_class' => AddressBook::class,\n            'birthday_calendar_enabled' => true,\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/Form/CalendarInstanceType.php",
    "content": "<?php\n\nnamespace App\\Form;\n\nuse App\\Entity\\CalendarInstance;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\ChoiceType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\SubmitType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextareaType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nuse Symfony\\Component\\OptionsResolver\\OptionsResolver;\n\nclass CalendarInstanceType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options): void\n    {\n        $builder\n            ->add('principalUri', HiddenType::class, [\n                'required' => true,\n            ])\n            ->add('uri', TextType::class, [\n                'label' => 'form.uri',\n                'disabled' => !$options['new'],\n                'help' => 'form.uri.help.caldav',\n                'required' => true,\n            ])\n            ->add('public', ChoiceType::class, [\n                'label' => 'form.public',\n                'disabled' => $options['shared'],\n                'help' => 'form.public.help.caldav',\n                'required' => true,\n                'choices' => ['yes' => true, 'no' => false],\n            ])\n            ->add('displayName', TextType::class, [\n                'label' => 'form.displayName',\n                'help' => 'form.name.help.caldav',\n            ])\n            ->add('description', TextareaType::class, [\n                'label' => 'form.description',\n                'required' => false,\n            ])\n            ->add('calendarColor', TextType::class, [\n                'label' => 'form.color',\n                'required' => false,\n                'help' => 'form.color.help',\n                'attr' => ['placeholder' => '#RRGGBBAA'],\n            ])\n            ->add('events', CheckboxType::class, [\n                'label' => 'form.events',\n                'mapped' => false,\n                'disabled' => $options['shared'],\n                'help' => 'form.events.help',\n                'required' => false,\n            ])\n            ->add('todos', CheckboxType::class, [\n                'label' => 'form.todos',\n                'mapped' => false,\n                'disabled' => $options['shared'],\n                'help' => 'form.todos.help',\n                'required' => false,\n            ])\n            ->add('notes', CheckboxType::class, [\n                'label' => 'form.notes',\n                'mapped' => false,\n                'disabled' => $options['shared'],\n                'help' => 'form.notes.help',\n                'required' => false,\n            ])\n            ->add('save', SubmitType::class, [\n                'label' => 'save',\n            ]);\n\n        if (!$options['public_calendars_enabled']) {\n            $builder->remove('public');\n        }\n    }\n\n    public function configureOptions(OptionsResolver $resolver): void\n    {\n        $resolver->setDefaults([\n            'new' => false,\n            'shared' => false,\n            'data_class' => CalendarInstance::class,\n            'public_calendars_enabled' => true,\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/Form/UserType.php",
    "content": "<?php\n\nnamespace App\\Form;\n\nuse App\\Entity\\User;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\PasswordType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\RepeatedType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\SubmitType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nuse Symfony\\Component\\OptionsResolver\\OptionsResolver;\n\nclass UserType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options): void\n    {\n        $builder\n            ->add('username', TextType::class, [\n                'label' => 'form.username',\n                'disabled' => !$options['new'],\n                'help' => 'form.username.help',\n            ])\n            ->add('displayName', TextType::class, [\n                'label' => 'form.displayName',\n                'mapped' => false,\n            ])\n            ->add('email', EmailType::class, [\n                'label' => 'form.email',\n                'mapped' => false,\n            ])\n            ->add('password', RepeatedType::class, [\n                'type' => PasswordType::class,\n                'invalid_message' => 'form.password.match',\n                'options' => ['attr' => ['class' => 'password-field', 'placeholder' => $options['new'] ? '' : 'form.password.empty']],\n                'required' => $options['new'],\n                'first_options' => ['label' => 'form.password'],\n                'second_options' => ['label' => 'form.password.repeat'],\n            ])\n            ->add('isAdmin', CheckboxType::class, [\n                'label' => 'form.admin',\n                'help' => 'form.admin.help',\n                'required' => false,\n                'mapped' => false,\n            ])\n            ->add('save', SubmitType::class, [\n                'label' => 'save',\n            ]);\n    }\n\n    public function configureOptions(OptionsResolver $resolver): void\n    {\n        $resolver->setDefaults([\n            'new' => false,\n            'data_class' => User::class,\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/Kernel.php",
    "content": "<?php\n\nnamespace App;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Kernel\\MicroKernelTrait;\nuse Symfony\\Component\\HttpKernel\\Kernel as BaseKernel;\n\nclass Kernel extends BaseKernel\n{\n    use MicroKernelTrait;\n\n    public function boot(): void\n    {\n        parent::boot();\n        $timezone = $this->getContainer()->getParameter('timezone');\n        if ('' === $timezone) {\n            return;\n        }\n        try {\n            date_default_timezone_set($timezone);\n        } catch (\\Exception $e) {\n            // We don't crash the app, the setting will be flagged as incorrect in the dashboard\n        }\n    }\n}\n"
  },
  {
    "path": "src/Logging/Monolog/PasswordFilterProcessor.php",
    "content": "<?php\n\nnamespace App\\Logging\\Monolog;\n\nuse Monolog\\LogRecord;\nuse Monolog\\Processor\\ProcessorInterface;\n\nfinal class PasswordFilterProcessor implements ProcessorInterface\n{\n    private const REDACTED = '****';\n    private const PASSWORD_KEYS = ['pass', 'password'];\n    private const SENSITIVE_ARGS_FUNCTIONS = ['validateUserPass', 'ldapOpen', 'password_verify', 'imapOpen', 'ldap_bind', 'hashPassword', 'dav'];\n\n    public static function redactContextRecursive(array $context): array\n    {\n        // Remove potentially sensitive data from function arguments\n        $shouldRedactArgs = array_key_exists('function', $context) && in_array($context['function'], self::SENSITIVE_ARGS_FUNCTIONS);\n\n        foreach ($context as $key => $item) {\n            if (in_array(strtolower($key), self::PASSWORD_KEYS) || ('args' === $key && $shouldRedactArgs)) {\n                $context[$key] = self::REDACTED;\n            } elseif (is_array($item)) {\n                $context[$key] = static::redactContextRecursive($item);\n            }\n        }\n\n        return $context;\n    }\n\n    public function __invoke(LogRecord $record): LogRecord\n    {\n        $context = $record->context;\n\n        $redactedContext = static::redactContextRecursive($context);\n\n        return $record->with(context: $redactedContext);\n    }\n}\n"
  },
  {
    "path": "src/Plugins/BirthdayCalendarPlugin.php",
    "content": "<?php\n\nnamespace App\\Plugins;\n\nuse App\\Services\\BirthdayService;\nuse Sabre\\CalDAV\\Backend\\PDO as CalendarBackend;\nuse Sabre\\CardDAV;\nuse Sabre\\DAV;\n\nclass BirthdayCalendarPlugin extends DAV\\ServerPlugin\n{\n    /**\n     * @var BirthdayService\n     */\n    protected $birthdayService;\n\n    /**\n     * @var DAV\\Server\n     */\n    protected $server;\n\n    public function __construct(BirthdayService $birthdayService, CalendarBackend $calendarBackend)\n    {\n        $this->birthdayService = $birthdayService;\n        $this->birthdayService->setBackend($calendarBackend);\n    }\n\n    public function initialize(DAV\\Server $server)\n    {\n        $this->server = $server;\n\n        // Hook into card creation\n        $server->on('afterCreateFile', [$this, 'afterCardCreate']);\n\n        // Hook into card updates\n        $server->on('afterWriteContent', [$this, 'afterCardUpdate']);\n\n        // Hook into card deletion\n        // Note: The node no longer exists at afterCardDelete so we\n        // use beforeCardDelete for simplicity\n        $server->on('beforeUnbind', [$this, 'beforeCardDelete']);\n    }\n\n    public function afterCardCreate(string $path, DAV\\ICollection $parentNode): void\n    {\n        if (!$parentNode instanceof CardDAV\\AddressBook) {\n            return;\n        }\n        $this->handleCardChange($path, $parentNode);\n    }\n\n    public function afterCardUpdate(string $path, DAV\\IFile $node): void\n    {\n        if (!$node instanceof CardDAV\\ICard) {\n            return;\n        }\n        $parentPath = dirname($path);\n        $parentNode = $this->server->tree->getNodeForPath($parentPath);\n\n        if (!$parentNode instanceof CardDAV\\AddressBook) {\n            return;\n        }\n\n        $this->handleCardChange($path, $parentNode);\n    }\n\n    public function beforeCardDelete(string $path): void\n    {\n        $node = $this->server->tree->getNodeForPath($path);\n\n        if (!$node instanceof CardDAV\\ICard) {\n            return;\n        }\n\n        $parentPath = dirname($path);\n        $parentNode = $this->server->tree->getNodeForPath($parentPath);\n\n        if (!$parentNode instanceof CardDAV\\AddressBook) {\n            return;\n        }\n\n        $addressBookId = $parentNode->getProperties(['id'])['id'];\n\n        $this->birthdayService->onCardDeleted($addressBookId, basename($path));\n    }\n\n    private function handleCardChange(string $path, CardDAV\\AddressBook $parentNode): void\n    {\n        $cardUri = basename($path);\n        $addressBookId = $parentNode->getProperties(['id'])['id'];\n        $cardNode = $this->server->tree->getNodeForPath($path);\n\n        $this->birthdayService->onCardChanged($addressBookId, $cardUri, $cardNode->get());\n    }\n\n    public function getPluginName(): string\n    {\n        return 'birthday-calendar';\n    }\n}\n"
  },
  {
    "path": "src/Plugins/DavisIMipPlugin.php",
    "content": "<?php\n\nnamespace App\\Plugins;\n\nuse DantSu\\OpenStreetMapStaticAPI\\LatLng;\nuse DantSu\\OpenStreetMapStaticAPI\\Markers;\nuse DantSu\\OpenStreetMapStaticAPI\\OpenStreetMap;\nuse Sabre\\CalDAV\\Schedule\\IMipPlugin as SabreBaseIMipPlugin;\nuse Sabre\\DAV;\nuse Sabre\\VObject\\ITip;\nuse Symfony\\Bridge\\Twig\\Mime\\TemplatedEmail;\nuse Symfony\\Component\\Mailer\\MailerInterface;\nuse Symfony\\Component\\Mime\\Address;\n\n/**\n * iMIP handler.\n */\nfinal class DavisIMipPlugin extends SabreBaseIMipPlugin\n{\n    public const MESSAGE_ORIGIN_INDICATOR = '(via Davis)';\n\n    /**\n     * @var MailerInterface\n     */\n    private $mailer;\n\n    /**\n     * @var string\n     */\n    protected $senderEmail;\n\n    /**\n     * @var string\n     */\n    protected $publicDir;\n\n    /**\n     * Creates the email handler.\n     */\n    public function __construct(MailerInterface $mailer, string $senderEmail, string $publicDir)\n    {\n        $this->mailer = $mailer;\n        $this->senderEmail = $senderEmail;\n        $this->publicDir = $publicDir;\n    }\n\n    /**\n     * Event handler for the 'schedule' event.\n     */\n    public function schedule(ITip\\Message $itip)\n    {\n        // Not sending any emails if the system considers the update\n        // insignificant.\n        if (!$itip->significantChange) {\n            if (empty($itip->scheduleStatus)) {\n                $itip->scheduleStatus = '1.0;We got the message, but it\\'s not significant enough to warrant an email';\n            }\n\n            return;\n        }\n\n        $summary = $itip->message->VEVENT->SUMMARY;\n\n        if ('mailto' !== parse_url($itip->sender, PHP_URL_SCHEME)\n            || 'mailto' !== parse_url($itip->recipient, PHP_URL_SCHEME)) {\n            return;\n        }\n\n        $deliveredLocally = '1.2' === $itip->getScheduleStatus();\n\n        // 7 is the length of `mailto:`.\n        $senderEmail = substr($itip->sender, 7);\n        $recipientEmail = substr($itip->recipient, 7);\n\n        // We fallback the senderName to the email if the iTIP originator does not send a name along.\n        $senderName = $itip->senderName ?? $senderEmail;\n        $recipientName = $itip->recipientName;\n\n        $subject = 'CalDAV message';\n        switch (strtoupper($itip->method)) {\n            case 'REPLY':\n                // In the case of a reply, we need to find the `PARTSTAT` from\n                // the user.\n                $partstat = (string) $itip->message->VEVENT->ATTENDEE['PARTSTAT'];\n                switch (strtoupper($partstat)) {\n                    case 'DECLINED':\n                        $subject = $senderName.' declined your invitation to \"'.$summary.'\"';\n                        $action = 'DECLINED';\n                        break;\n                    case 'ACCEPTED':\n                        $subject = $senderName.' accepted your invitation to \"'.$summary.'\"';\n                        $action = 'ACCEPTED';\n                        break;\n                    case 'TENTATIVE':\n                        $subject = $senderName.' tentatively accepted your invitation to \"'.$summary.'\"';\n                        $action = 'TENTATIVE';\n                        break;\n                    default:\n                        $itip->scheduleStatus = '5.0;Email not delivered. We didn\\'t understand this PARTSTAT.';\n\n                        return;\n                }\n\n                break;\n            case 'REQUEST':\n                $subject = $senderName.' invited you to \"'.$summary.'\"';\n                $action = 'REQUEST';\n                break;\n            case 'CANCEL':\n                $subject = '\"'.$summary.'\" has been canceled.';\n                $action = 'CANCEL';\n                break;\n        }\n\n        // Construct objects for the mail template\n        $dateTime =\n            isset($itip->message->VEVENT->DTSTART)\n                ? $itip->message->VEVENT->DTSTART->getDateTime()\n                : new \\DateTime('now');\n\n        $allDay =\n            isset($itip->message->VEVENT->DTSTART)\n            && false === $itip->message->VEVENT->DTSTART->hasTime();\n\n        $attendees = [];\n        if (isset($itip->message->VEVENT->ATTENDEE)) {\n            $_attendees = &$itip->message->VEVENT->ATTENDEE;\n            for ($i = 0, $max = count($_attendees); $i < $max; ++$i) {\n                $attendee = $_attendees[$i];\n                $attendees[] = [\n                    'cn' => isset($attendee['CN'])\n                            ? (string) $attendee['CN']\n                            : (string) $attendee['EMAIL'],\n                    'email' => isset($attendee['EMAIL'])\n                            ? (string) $attendee['EMAIL']\n                            : null,\n                    'role' => isset($attendee['ROLE'])\n                            ? (string) $attendee['ROLE']\n                            : null,\n                ];\n            }\n            usort($attendees, function ($a, $b) {\n                if ('CHAIR' === $a['role']) {\n                    return -1;\n                }\n\n                return 1;\n            });\n        }\n\n        $notEmpty = function ($property, $else) use ($itip) {\n            if (isset($itip->message->VEVENT->$property)) {\n                $handle = (string) $itip->message->VEVENT->$property;\n                if (!empty($handle)) {\n                    return $handle;\n                }\n            }\n\n            return $else;\n        };\n\n        $url = $notEmpty('URL', false);\n        $description = $notEmpty('DESCRIPTION', false);\n        $location = $notEmpty('LOCATION', false);\n        $locationImageDataAsBase64 = false;\n        $locationLink = false;\n\n        if (isset($itip->message->VEVENT->{'X-APPLE-STRUCTURED-LOCATION'})) {\n            $match = preg_match(\n                '/^(geo:)?(?<latitude>\\-?\\d+\\.\\d+),(?<longitude>\\-?\\d+\\.\\d+)$/',\n                (string) $itip->message->VEVENT->{'X-APPLE-STRUCTURED-LOCATION'},\n                $coordinates\n            );\n            if (0 !== $match) {\n                $zoom = 16;\n                $width = 500;\n                $height = 220;\n\n                $latLng = new LatLng($coordinates['latitude'], $coordinates['longitude']);\n\n                // https://github.com/DantSu/php-osm-static-api\n                $locationImageDataAsBase64 = (new OpenStreetMap($latLng, $zoom, $width, $height))\n                    ->addMarkers(\n                        (new Markers($this->publicDir.'/images/marker.png'))\n                            ->setAnchor(Markers::ANCHOR_CENTER, Markers::ANCHOR_BOTTOM)\n                            ->addMarker(new LatLng($coordinates['latitude'], $coordinates['longitude']))\n                    )\n                    ->getImage()\n                    ->getBase64PNG();\n\n                $locationLink =\n                    'https://www.openstreetmap.org'.\n                    '/?mlat='.$coordinates['latitude'].\n                    '&mlon='.$coordinates['longitude'].\n                    '#map='.$zoom.\n                    '/'.$coordinates['latitude'].\n                    '/'.$coordinates['longitude'];\n            }\n        }\n\n        // For the mail headers, we don't automatically default to the email\n        if ($itip->senderName) {\n            // If we have a proper name, we use it with the optional message origin indicator\n            $mailSenderName = $itip->senderName;\n            if (static::MESSAGE_ORIGIN_INDICATOR) {\n                $mailSenderName = $mailSenderName.' '.static::MESSAGE_ORIGIN_INDICATOR;\n            }\n        } elseif (static::MESSAGE_ORIGIN_INDICATOR) {\n            // Otherwise, we don't use the email except if we explicitly have a message origin\n            // indicator (it would be redundant if there wasn't)\n            $mailSenderName = $senderEmail.' '.static::MESSAGE_ORIGIN_INDICATOR;\n        }\n\n        $message = (new TemplatedEmail())\n            ->from(new Address($this->senderEmail, $mailSenderName))\n            ->to(new Address($recipientEmail, $recipientName ?? ''))\n            ->replyTo(new Address($senderEmail, $mailSenderName))\n            ->subject($subject);\n\n        if (DAV\\Server::$exposeVersion) {\n            $message->getHeaders()\n                    ->addTextHeader('X-Sabre-Version: ', DAV\\Version::VERSION)\n                    ->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply');\n        }\n\n        // Now that we have everything, we can set the message body\n        $message->htmlTemplate('mails/scheduling.html.twig')\n                ->textTemplate('mails/scheduling.txt.twig')\n                ->context([\n                    'senderName' => $senderName,\n                    'summary' => $summary,\n                    'action' => $action,\n                    'dateTime' => $dateTime,\n                    'allDay' => $allDay,\n                    'attendees' => $attendees,\n                    'location' => $location,\n                    'locationImageDataAsBase64' => $locationImageDataAsBase64,\n                    'locationLink' => $locationLink,\n                    'url' => $url,\n                    'description' => $description,\n                ]);\n\n        if (false === $deliveredLocally) {\n            // Attach the event file (invite.ics)\n            $message->attach($itip->message->serialize(), 'invite.ics', 'text/calendar; method='.(string) $itip->method.'; charset=UTF-8');\n        }\n\n        $this->mailer->send($message);\n\n        if (false === $deliveredLocally) {\n            $itip->scheduleStatus = '1.1;Scheduling message is sent via iMip.';\n        }\n    }\n\n    /**\n     * Returns a bunch of meta-data about the plugin.\n     *\n     * Providing this information is optional, and is mainly displayed by the\n     * Browser plugin.\n     *\n     * The description key in the returned array may contain html and will not\n     * be sanitized.\n     *\n     * @return array\n     */\n    public function getPluginInfo()\n    {\n        return [\n            'name' => $this->getPluginName(),\n            'description' => 'HTML Email delivery (rfc6047) for CalDAV scheduling',\n            'link' => 'http://github.com/tchapi/davis',\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Plugins/PublicAwareDAVACLPlugin.php",
    "content": "<?php\n\nnamespace App\\Plugins;\n\nuse App\\Entity\\CalendarInstance;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Sabre\\HTTP\\RequestInterface;\nuse Sabre\\HTTP\\ResponseInterface;\n\nclass PublicAwareDAVACLPlugin extends \\Sabre\\DAVACL\\Plugin\n{\n    /**\n     * @var EntityManagerInterface\n     */\n    protected $em;\n\n    /**\n     * @var bool\n     */\n    protected $public_calendars_enabled;\n\n    public function __construct(EntityManagerInterface $entityManager, bool $public_calendars_enabled)\n    {\n        $this->em = $entityManager;\n        $this->public_calendars_enabled = $public_calendars_enabled;\n    }\n\n    /**\n     * We override this method so that public objects can be seen correctly in the browser,\n     * with the assets (css, images).\n     */\n    public function beforeMethod(RequestInterface $request, ResponseInterface $response)\n    {\n        $params = $request->getQueryParameters();\n        if (isset($params['sabreAction']) && 'asset' === $params['sabreAction']) {\n            return;\n        }\n\n        return parent::beforeMethod($request, $response);\n    }\n\n    public function getAcl($node): array\n    {\n        // Note:\n        // '{DAV:}unauthenticated' - only unauthenticated users\n        // '{DAV:}all' - all users (both authenticated and unauthenticated)\n        // '{DAV:}authenticated' - only authenticated users\n        $acl = parent::getAcl($node);\n\n        if ($this->public_calendars_enabled) {\n            // Handle both Calendar AND SharedCalendar (which extends Calendar)\n            if ($node instanceof \\Sabre\\CalDAV\\Calendar || $node instanceof \\Sabre\\CalDAV\\CalendarObject) {\n                // The property is private in \\Sabre\\CalDAV\\CalendarObject and we don't want to create\n                // a new class just to access it, so we use a closure.\n                $calendarInfo = (fn () => $this->calendarInfo)->call($node);\n                // [0] is the calendarId, [1] is the calendarInstanceId\n                $calendarInstanceId = $calendarInfo['id'][1];\n\n                $calendar = $this->em->getRepository(CalendarInstance::class)->findOneById($calendarInstanceId);\n\n                if ($calendar && $calendar->isPublic()) {\n                    // Add unauthenticated read access on the object itself\n                    $acl[] = [\n                        'principal' => '{DAV:}unauthenticated',\n                        'privilege' => '{DAV:}read',\n                        'protected' => false,\n                    ];\n                }\n            }\n        }\n\n        return $acl;\n    }\n}\n"
  },
  {
    "path": "src/Repository/CalendarInstanceRepository.php",
    "content": "<?php\n\nnamespace App\\Repository;\n\nuse App\\Entity\\Calendar;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\CalendarObject;\nuse App\\Entity\\Principal;\nuse App\\Entity\\SchedulingObject;\nuse Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;\nuse Doctrine\\Persistence\\ManagerRegistry;\n\n/**\n * @method CalendarInstance|null find($id, $lockMode = null, $lockVersion = null)\n * @method CalendarInstance|null findOneBy(array $criteria, array $orderBy = null)\n * @method CalendarInstance[]    findAll()\n * @method CalendarInstance[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)\n */\nclass CalendarInstanceRepository extends ServiceEntityRepository\n{\n    public function __construct(ManagerRegistry $registry)\n    {\n        parent::__construct($registry, CalendarInstance::class);\n    }\n\n    /**\n     * @return CalendarInstance[] Returns an array of CalendarInstance objects\n     */\n    public function findSharedInstancesOfInstance(int $calendarId, bool $withCalendar = false)\n    {\n        $query = $this->createQueryBuilder('c')\n            ->leftJoin(Principal::class, 'p', \\Doctrine\\ORM\\Query\\Expr\\Join::WITH, 'c.principalUri = p.uri')\n            ->where('c.calendar = :id')\n            ->setParameter('id', $calendarId)\n            ->andWhere('c.access NOT IN (:ownerAccess)')\n            ->setParameter('ownerAccess', CalendarInstance::getOwnerAccesses());\n\n        if ($withCalendar) {\n            // Returns CalendarInstances as arrays, with displayName and email of the owner\n            return $query->addSelect('p.displayName', 'p.email')\n                ->getQuery()\n                ->getArrayResult();\n        }\n\n        // Returns CalendarInstances as objects\n        return $query->getQuery()\n            ->getResult();\n    }\n\n    /**\n     * @return CalendarInstance Returns a CalendarInstance object\n     */\n    public function findSharedInstanceOfInstanceFor(int $calendarId, string $principalUri)\n    {\n        return $this->createQueryBuilder('c')\n            ->where('c.calendar = :id')\n            ->setParameter('id', $calendarId)\n            ->andWhere('c.access NOT IN (:ownerAccess)')\n            ->setParameter('ownerAccess', CalendarInstance::getOwnerAccesses())\n            ->andWhere('c.principalUri = :principalUri')\n            ->setParameter('principalUri', $principalUri)\n            ->getQuery()\n            ->getOneOrNullResult();\n    }\n\n    public function hasDifferentOwner(int $calendarId, string $principalUri): bool\n    {\n        return $this->createQueryBuilder('c')\n            ->select('COUNT(c.id)')\n            ->where('c.calendar = :id')\n            ->setParameter('id', $calendarId)\n            ->andWhere('c.access IN (:ownerAccess)')\n            ->setParameter('ownerAccess', CalendarInstance::getOwnerAccesses())\n            ->andWhere('c.principalUri != :principalUri')\n            ->setParameter('principalUri', $principalUri)\n            ->getQuery()\n            ->getSingleScalarResult() > 0;\n    }\n\n\n    public function findAllSchedulingObjectsForCalendar(int $calendarInstanceId, string $principalUri): array\n    {\n        $objectRepository = $this->getEntityManager()->getRepository(SchedulingObject::class);\n        return $objectRepository->createQueryBuilder('s')\n            ->leftJoin(CalendarObject::class, 'c', \\Doctrine\\ORM\\Query\\Expr\\Join::WITH, 'c.uri = s.uri')\n            ->leftJoin(CalendarInstance::class, 'ci', \\Doctrine\\ORM\\Query\\Expr\\Join::WITH, 'ci.calendar = c.calendar')\n            ->where('ci.id = :id')\n            // uri is not unique across calendars — two different calendars can have objects with the same uri.\n            // The join should also filter by principaluri as a consequence\n            ->andWhere('s.principalUri = :principalUri')\n            ->setParameter('id', $calendarInstanceId)\n            ->setParameter('principalUri', $principalUri)\n            ->getQuery()\n            ->getResult();\n    }\n\n    /**\n     * Get counts of calendar objects by component type for a calendar instance.\n     *\n     * @param int $calendarId The ID of the calendar\n     *\n     * @return array An associative array with keys 'events', 'notes', 'tasks' containing their respective counts\n     */\n    public function getObjectCountsByComponentType(int $calendarId): array\n    {\n        $objectRepository = $this->getEntityManager()->getRepository(CalendarObject::class);\n\n        // Instead of three separate queries, get all counts in a single query\n        $results = $objectRepository->createQueryBuilder('o')\n            ->select('o.componentType, COUNT(o.id) as count')\n            ->where('o.calendar = :calendarId')\n            ->setParameter('calendarId', $calendarId)\n            ->groupBy('o.componentType')\n            ->getQuery()\n            ->getResult();\n\n        $componentTypeMap = [\n            Calendar::COMPONENT_EVENTS => 'events',\n            Calendar::COMPONENT_NOTES => 'notes',\n            Calendar::COMPONENT_TODOS => 'tasks',\n        ];\n\n        $counts = [\n            'events' => 0,\n            'notes' => 0,\n            'tasks' => 0,\n        ];\n\n        // Map query results to the expected keys\n        foreach ($results as $result) {\n            if (isset($componentTypeMap[$result['componentType']])) {\n                $counts[$componentTypeMap[$result['componentType']]] = (int) $result['count'];\n            }\n        }\n\n        return $counts;\n    }\n}\n"
  },
  {
    "path": "src/Repository/PrincipalRepository.php",
    "content": "<?php\n\nnamespace App\\Repository;\n\nuse App\\Entity\\Principal;\nuse Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;\nuse Doctrine\\Persistence\\ManagerRegistry;\n\n/**\n * @method Principal|null find($id, $lockMode = null, $lockVersion = null)\n * @method Principal|null findOneBy(array $criteria, array $orderBy = null)\n * @method Principal[]    findAll()\n * @method Principal[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)\n */\nclass PrincipalRepository extends ServiceEntityRepository\n{\n    public function __construct(ManagerRegistry $registry)\n    {\n        parent::__construct($registry, Principal::class);\n    }\n\n    /**\n     * @return Principal[] Returns an array of Principal objects\n     */\n    public function findAllExceptPrincipal(string $principalUri)\n    {\n        return $this->createQueryBuilder('p')\n            ->andWhere('p.isMain = :isMain')\n            ->andWhere('p.uri <> :val')\n            ->setParameter('isMain', true)\n            ->setParameter('val', $principalUri)\n            ->getQuery()\n            ->getResult();\n    }\n\n    /**\n     * @return array<array{Principal, userId: int}>\n     */\n    public function findAllMainPrincipalsWithUserIds(): array\n    {\n        return $this->createQueryBuilder('p')\n            ->addSelect('u.id AS userId')\n            ->leftJoin(\n                \\App\\Entity\\User::class,\n                'u',\n                \\Doctrine\\ORM\\Query\\Expr\\Join::WITH,\n                'CONCAT(:prefix, u.username) = p.uri'\n            )\n            ->andWhere('p.isMain = :isMain')\n            ->setParameter('isMain', true)\n            ->setParameter('prefix', Principal::PREFIX)\n            ->getQuery()\n            ->getResult();\n    }\n}\n"
  },
  {
    "path": "src/Security/AdminUser.php",
    "content": "<?php\n\nnamespace App\\Security;\n\nuse Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\n\nclass AdminUser implements UserInterface, PasswordAuthenticatedUserInterface\n{\n    private $username;\n    private $password;\n\n    public function __construct(string $username, string $password)\n    {\n        $this->username = $username;\n        $this->password = $password;\n    }\n\n    /**\n     * @return (Role|string)[] The user roles\n     */\n    public function getRoles(): array\n    {\n        return ['ROLE_ADMIN'];\n    }\n\n    /**\n     * Returns the password used to authenticate the user.\n     */\n    public function getPassword(): string\n    {\n        return $this->password;\n    }\n\n    /**\n     * Returns the salt that was originally used to encode the password.\n     *\n     * This can return null if the password was not encoded using a salt.\n     *\n     * @return string|null The salt\n     */\n    public function getSalt()\n    {\n        return null;\n    }\n\n    /**\n     * Returns the username used to authenticate the user.\n     *\n     * @return string The username\n     */\n    public function getUsername()\n    {\n        return $this->username;\n    }\n\n    public function getUserIdentifier(): string\n    {\n        return $this->username;\n    }\n\n    /**\n     * Removes sensitive data from the user.\n     *\n     * This is important if, at any given point, sensitive information like\n     * the plain-text password is stored on this object.\n     */\n    public function eraseCredentials(): void\n    {\n    }\n}\n"
  },
  {
    "path": "src/Security/AdminUserProvider.php",
    "content": "<?php\n\nnamespace App\\Security;\n\nuse Symfony\\Component\\Security\\Core\\Exception\\UnsupportedUserException;\nuse Symfony\\Component\\Security\\Core\\Exception\\UsernameNotFoundException;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\nuse Symfony\\Component\\Security\\Core\\User\\UserProviderInterface;\n\nclass AdminUserProvider implements UserProviderInterface\n{\n    /**\n     * Symfony calls this method if you use features like switch_user\n     * or remember_me.\n     *\n     * If you're not using these features, you do not need to implement\n     * this method.\n     *\n     * @throws UsernameNotFoundException if the user is not found\n     *\n     * @return UserInterface\n     */\n    public function loadUserByUsername($username)\n    {\n        throw new \\Exception('Not implemented, because not needed');\n    }\n\n    public function loadUserByIdentifier(string $identifier): UserInterface\n    {\n        return new AdminUser($identifier, bin2hex(random_bytes(64)));\n    }\n\n    /**\n     * Refreshes the user after being reloaded from the session.\n     *\n     * When a user is logged in, at the beginning of each request, the\n     * User object is loaded from the session and then this method is\n     * called. Your job is to make sure the user's data is still fresh by,\n     * for example, re-querying for fresh User data.\n     *\n     * If your firewall is \"stateless: true\" (for a pure API), this\n     * method is not called.\n     */\n    public function refreshUser(UserInterface $user): UserInterface\n    {\n        if (!$user instanceof AdminUser) {\n            throw new UnsupportedUserException(sprintf('Invalid user class \"%s\".', get_class($user)));\n        }\n\n        return $user;\n    }\n\n    /**\n     * Tells Symfony to use this provider for this User class.\n     */\n    public function supportsClass($class): bool\n    {\n        return AdminUser::class === $class;\n    }\n}\n"
  },
  {
    "path": "src/Security/ApiKeyAuthenticator.php",
    "content": "<?php\n\nnamespace App\\Security;\n\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface;\nuse Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException;\nuse Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException;\nuse Symfony\\Component\\Security\\Http\\Authenticator\\AbstractAuthenticator;\nuse Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\UserBadge;\nuse Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Passport;\nuse Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\SelfValidatingPassport;\n\nclass ApiKeyAuthenticator extends AbstractAuthenticator\n{\n    private string $apiKey;\n\n    public function __construct(string $apiKey)\n    {\n        // Disable API endpoint if no API key is set\n        if (hash_equals('', trim($apiKey))) {\n            throw new \\LogicException('API endpoint is disabled.');\n        }\n\n        $this->apiKey = $apiKey;\n    }\n\n    public function supports(Request $request): ?bool\n    {\n        // Skip authentication for public health endpoint\n        if (preg_match('#^/api/v1/health$#', $request->getPathInfo())) {\n            return false;\n        }\n\n        // Always attempt to authenticate even if no API token is provided in the request\n        // This stops the login page from being shown when accessing API routes\n        return true;\n    }\n\n    public function authenticate(Request $request): Passport\n    {\n        $apiToken = $request->headers->get('X-Davis-API-Token');\n        if (null === $apiToken) {\n            throw new CustomUserMessageAuthenticationException('Missing X-Davis-API-Token header');\n        }\n\n        if (false === hash_equals($this->apiKey, $apiToken)) {\n            throw new CustomUserMessageAuthenticationException('Invalid X-Davis-API-Token header');\n        }\n\n        return new SelfValidatingPassport(new UserBadge('X-DAVIS-API'));\n    }\n\n    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response\n    {\n        return null;\n    }\n\n    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response\n    {\n        $data = [\n            'status' => 'error',\n            'message' => $exception->getMessage(),\n            'timestamp' => date('c'),\n        ];\n\n        return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);\n    }\n}\n"
  },
  {
    "path": "src/Security/LoginFormAuthenticator.php",
    "content": "<?php\n\nnamespace App\\Security;\n\nuse Symfony\\Component\\HttpFoundation\\RedirectResponse;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\nuse Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface;\nuse Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException;\nuse Symfony\\Component\\Security\\Csrf\\CsrfTokenManagerInterface;\nuse Symfony\\Component\\Security\\Http\\Authenticator\\AbstractLoginFormAuthenticator;\nuse Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\CsrfTokenBadge;\nuse Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\UserBadge;\nuse Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Passport;\nuse Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\SelfValidatingPassport;\nuse Symfony\\Component\\Security\\Http\\SecurityRequestAttributes;\nuse Symfony\\Component\\Security\\Http\\Util\\TargetPathTrait;\n\nclass LoginFormAuthenticator extends AbstractLoginFormAuthenticator\n{\n    use TargetPathTrait;\n\n    private $urlGenerator;\n    private $csrfTokenManager;\n\n    private $adminLogin;\n    private $adminPassword;\n\n    public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, string $adminLogin, string $adminPassword)\n    {\n        $this->urlGenerator = $urlGenerator;\n        $this->csrfTokenManager = $csrfTokenManager;\n        $this->adminLogin = $adminLogin;\n        $this->adminPassword = $adminPassword;\n    }\n\n    protected function getLoginUrl(Request $request): string\n    {\n        return $this->urlGenerator->generate('app_login');\n    }\n\n    public function supports(Request $request): bool\n    {\n        return 'app_login' === $request->attributes->get('_route')\n            && $request->isMethod('POST');\n    }\n\n    public function authenticate(Request $request): Passport\n    {\n        $credentials = [\n            'username' => $request->request->get('_username'),\n            'password' => $request->request->get('_password'),\n            'csrf_token' => $request->request->get('_csrf_token'),\n        ];\n        $request->getSession()->set(\n            SecurityRequestAttributes::LAST_USERNAME,\n            $credentials['username']\n        );\n\n        if ($credentials['username'] !== $this->adminLogin) {\n            // fail authentication with a custom error\n            throw new CustomUserMessageAuthenticationException('Username could not be found.');\n        }\n\n        if ($credentials['password'] !== $this->adminPassword) {\n            // fail authentication with a custom error\n            throw new CustomUserMessageAuthenticationException('Invalid credentials.');\n        }\n\n        return new SelfValidatingPassport(\n            new UserBadge($this->adminLogin),\n            [new CsrfTokenBadge('authenticate', $credentials['csrf_token'])]\n        );\n    }\n\n    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response\n    {\n        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {\n            return new RedirectResponse($targetPath);\n        }\n\n        return new RedirectResponse($this->urlGenerator->generate('dashboard'));\n    }\n}\n"
  },
  {
    "path": "src/Services/BasicAuth.php",
    "content": "<?php\n\nnamespace App\\Services;\n\nuse App\\Entity\\User;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Sabre\\DAV\\Auth\\Backend\\AbstractBasic;\n\nfinal class BasicAuth extends AbstractBasic\n{\n    /**\n     * Utils class.\n     *\n     * @var Utils\n     */\n    private $utils;\n\n    /**\n     * Doctrine registry.\n     *\n     * @var ManagerRegistry\n     */\n    private $doctrine;\n\n    public function __construct(ManagerRegistry $doctrine, Utils $utils)\n    {\n        $this->utils = $utils;\n        $this->doctrine = $doctrine;\n    }\n\n    protected function validateUserPass($username, $password): bool\n    {\n        $user = $this->doctrine->getRepository(User::class)->findOneByUsername($username);\n\n        if (!$user) {\n            return false;\n        }\n\n        if ('$2y$' === substr($user->getPassword(), 0, 4)) {\n            // Use password_verify with secure passwords\n            return password_verify($password, $user->getPassword());\n        }\n\n        // Use unsecure legacy password hashing (from legacy sabre/dav implementation)\n        return $user->getPassword() === $this->utils->hashPassword($username, $password);\n    }\n}\n"
  },
  {
    "path": "src/Services/BirthdayService.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\n/**\n * Largely inspired by https://github.com/nextcloud/server/blob/master/apps/dav/lib/CalDAV/BirthdayService.php which is licensed in these terms:\n * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors\n * SPDX-FileCopyrightText: 2016 ownCloud, Inc.\n * SPDX-License-Identifier: AGPL-3.0-only\n */\n\nnamespace App\\Services;\n\nuse App\\Constants;\nuse App\\Entity\\AddressBook;\nuse App\\Entity\\Calendar;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\CalendarObject;\nuse App\\Entity\\Card;\nuse App\\Entity\\Principal;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Sabre\\CalDAV\\Backend\\PDO as CalendarBackend;\nuse Sabre\\DAV\\Sharing\\Plugin as SharingPlugin;\nuse Sabre\\VObject\\Component\\VCalendar;\nuse Sabre\\VObject\\Component\\VCard;\nuse Sabre\\VObject\\DateTimeParser;\nuse Sabre\\VObject\\Document;\nuse Sabre\\VObject\\InvalidDataException;\nuse Sabre\\VObject\\Property\\VCard\\DateAndOrTime;\nuse Sabre\\VObject\\Reader;\n\nclass BirthdayService\n{\n    /**\n     * @var CalendarBackend\n     */\n    private $calendarBackend;\n\n    public function __construct(\n        private ManagerRegistry $doctrine,\n        private string $birthdayReminderOffset,\n    ) {\n    }\n\n    public function setBackend(CalendarBackend $calendarBackend)\n    {\n        $this->calendarBackend = $calendarBackend;\n    }\n\n    public function onCardChanged(int $addressBookId, string $cardUri, string $cardData): void\n    {\n        $book = $this->doctrine->getRepository(AddressBook::class)->findOneById($addressBookId);\n\n        if (!$book->isIncludedInBirthdayCalendar()) {\n            return;\n        }\n\n        $principalUri = $book->getPrincipalUri();\n        $calendarInstance = $this->ensureBirthdayCalendarExists($principalUri);\n\n        $this->updateCalendar($cardUri, $cardData, $book, $calendarInstance);\n    }\n\n    public function onCardDeleted(int $addressBookId, string $cardUri): void\n    {\n        $book = $this->doctrine->getRepository(AddressBook::class)->findOneById($addressBookId);\n\n        if (!$book->isIncludedInBirthdayCalendar()) {\n            return;\n        }\n\n        $principalUri = $book->getPrincipalUri();\n        $calendarInstance = $this->ensureBirthdayCalendarExists($principalUri);\n\n        $objectUri = $book->getUri().'-'.$cardUri.'.ics';\n\n        $calendar = $calendarInstance->getCalendar();\n        // This is the structure that needs to be passed to the backend methods\n        $calendarId = [$calendar->getId(), $calendarInstance->getId()];\n\n        $this->calendarBackend->deleteCalendarObject(\n            $calendarId,\n            $objectUri\n        );\n    }\n\n    public function shouldBirthdayCalendarExist(string $principalUri): bool\n    {\n        $addressbooks = $this->doctrine->getRepository(AddressBook::class)->findByPrincipalUri($principalUri);\n\n        return array_reduce($addressbooks, function ($carry, $addressbook) {\n            return $carry || $addressbook->isIncludedInBirthdayCalendar();\n        }, false);\n    }\n\n    public function ensureBirthdayCalendarExists(string $principalUri): CalendarInstance\n    {\n        $instance = $this->doctrine->getRepository(CalendarInstance::class)->findOneBy(['principalUri' => $principalUri, 'uri' => Constants::BIRTHDAY_CALENDAR_URI]);\n\n        if ($instance) {\n            return $instance;\n        }\n\n        $em = $this->doctrine->getManager();\n\n        $calendar = new Calendar();\n        $em->persist($calendar);\n\n        $instance = (new CalendarInstance())\n                    ->setPrincipalUri($principalUri)\n                    ->setDisplayName('🎁 Birthdays')\n                    ->setDescription('Birthdays')\n                    ->setAccess(SharingPlugin::ACCESS_READ)\n                    ->setCalendarOrder(0)\n                    ->setCalendar($calendar)\n                    ->setTransparent(1)\n                    ->setShareInviteStatus(SharingPlugin::INVITE_ACCEPTED)\n                    ->setUri(Constants::BIRTHDAY_CALENDAR_URI);\n\n        $em->persist($instance);\n        $em->flush();\n\n        return $instance;\n    }\n\n    public function deleteBirthdayCalendar(string $principalUri): void\n    {\n        $instance = $this->doctrine->getRepository(CalendarInstance::class)->findOneBy(['principalUri' => $principalUri, 'uri' => Constants::BIRTHDAY_CALENDAR_URI]);\n\n        if (!$instance) {\n            return;\n        }\n\n        $em = $this->doctrine->getManager();\n\n        $em->remove($instance);\n        $em->remove($instance->getCalendar());\n        $em->flush();\n    }\n\n    /**\n     * @throws InvalidDataException\n     */\n    public function buildDataFromContact(string $cardData): ?VCalendar\n    {\n        if (empty($cardData)) {\n            return null;\n        }\n\n        try {\n            $doc = Reader::read($cardData);\n            // We're always converting to vCard 4.0 so we can rely on the\n            // VCardConverter handling the X-APPLE-OMIT-YEAR property for us.\n            if (!$doc instanceof VCard) {\n                return null;\n            }\n            $doc = $doc->convert(Document::VCARD40);\n        } catch (\\Exception $e) {\n            return null;\n        }\n\n        if (!isset($doc->BDAY) || !isset($doc->FN)) {\n            return null;\n        }\n\n        $birthday = $doc->BDAY;\n        if (!(string) $birthday) {\n            return null;\n        }\n\n        // Skip if the BDAY property is not of the right type.\n        if (!$birthday instanceof DateAndOrTime) {\n            return null;\n        }\n\n        // Skip if we can't parse the BDAY value.\n        try {\n            $dateParts = DateTimeParser::parseVCardDateTime($birthday->getValue());\n        } catch (InvalidDataException $e) {\n            return null;\n        }\n\n        if (null !== $dateParts['year']) {\n            $parameters = $birthday->parameters();\n            $omitYear = (isset($parameters['X-APPLE-OMIT-YEAR']) && $parameters['X-APPLE-OMIT-YEAR'] === $dateParts['year']);\n            // 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown\n            if ($omitYear || 1604 === (int) $dateParts['year']) {\n                $dateParts['year'] = null;\n            }\n        }\n\n        $originalYear = null;\n        if (null !== $dateParts['year']) {\n            $originalYear = (int) $dateParts['year'];\n        }\n\n        try {\n            if ($birthday instanceof DateAndOrTime) {\n                $date = $birthday->getDateTime();\n            } else {\n                $date = new \\DateTimeImmutable($birthday);\n            }\n        } catch (\\Exception $e) {\n            return null;\n        }\n\n        $summary = '🎂 '.$doc->FN->getValue().($originalYear ? (' ('.$originalYear.')') : '');\n\n        $vCal = new VCalendar();\n        $vCal->VERSION = '2.0';\n        $vCal->PRODID = '-//IDN davis//Birthday calendar//EN';\n        $vEvent = $vCal->createComponent('VEVENT');\n        $vEvent->add('DTSTART');\n        $vEvent->DTSTART->setDateTime(\n            $date\n        );\n        $vEvent->DTSTART['VALUE'] = 'DATE';\n        $vEvent->add('DTEND');\n\n        $dtEndDate = (new \\DateTime())->setTimestamp($date->getTimeStamp());\n        $dtEndDate->add(new \\DateInterval('P1D'));\n        $vEvent->DTEND->setDateTime(\n            $dtEndDate\n        );\n\n        $vEvent->DTEND['VALUE'] = 'DATE';\n        $vEvent->{'UID'} = $doc->UID;\n\n        $leapDay = (2 === (int) $dateParts['month']\n                && 29 === (int) $dateParts['date']);\n        if (null === $dateParts['year'] || $originalYear < 1970) {\n            $birthday = ($leapDay ? '1972-' : '1970-')\n                .$dateParts['month'].'-'.$dateParts['date'];\n        }\n\n        if ($leapDay) {\n            /* Sabre\\VObject supports BYMONTHDAY only if BYMONTH\n             * is also set */\n            $vEvent->{'RRULE'} = 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1';\n        } else {\n            $vEvent->{'RRULE'} = 'FREQ=YEARLY';\n        }\n\n        $vEvent->{'SUMMARY'} = $summary;\n        $vEvent->{'TRANSP'} = 'TRANSPARENT';\n\n        // Set a reminder, if needed\n        if ('false' !== strtolower($this->birthdayReminderOffset)) {\n            $alarm = $vCal->createComponent('VALARM');\n            $alarm->add($vCal->createProperty('TRIGGER', $this->birthdayReminderOffset, ['VALUE' => 'DURATION']));\n            $alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));\n            $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));\n            $vEvent->add($alarm);\n        }\n\n        $vCal->add($vEvent);\n\n        return $vCal;\n    }\n\n    public function resetForPrincipal(string $principal): void\n    {\n        $calendarInstance = $this->doctrine->getRepository(CalendarInstance::class)->findOneBy(['principalUri' => $principal, 'uri' => Constants::BIRTHDAY_CALENDAR_URI]);\n\n        if (!$calendarInstance) {\n            return; // The user's birthday calendar doesn't exist, no need to purge it\n        }\n\n        $calendarObjects = $this->doctrine->getRepository(CalendarObject::class)->findByCalendar($calendarInstance->getCalendar());\n        $em = $this->doctrine->getManager();\n\n        foreach ($calendarObjects as $calendarObject) {\n            $em->remove($calendarObject);\n        }\n\n        $em->flush();\n    }\n\n    public function syncUser(string $username): void\n    {\n        $this->syncPrincipal(Principal::PREFIX.$username);\n    }\n\n    public function syncPrincipal(string $principal): void\n    {\n        if (!$this->shouldBirthdayCalendarExist($principal)) {\n            $this->deleteBirthdayCalendar($principal);\n\n            return;\n        }\n\n        $calendarInstance = $this->ensureBirthdayCalendarExists($principal);\n\n        // Reset the calendar\n        $this->resetForPrincipal($principal);\n\n        // Get all address books that should be included and iterate\n        $addressbooks = $this->doctrine->getRepository(AddressBook::class)->findBy(['principalUri' => $principal, 'includedInBirthdayCalendar' => true]);\n        foreach ($addressbooks as $book) {\n            $cards = $this->doctrine->getRepository(Card::class)->findByAddressBook($book);\n\n            foreach ($cards as $card) {\n                $this->onCardChanged($book->getId(), $card->getUri(), $card->getCardData());\n            }\n        }\n    }\n\n    public function birthdayEventChanged(string $existingCalendarData, VCalendar $newCalendarData): bool\n    {\n        try {\n            $existingBirthday = Reader::read($existingCalendarData);\n        } catch (\\Exception $ex) {\n            return true;\n        }\n\n        return\n            $newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue()\n            || $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()\n        ;\n    }\n\n    /**\n     * @throws InvalidDataException\n     */\n    private function updateCalendar(string $cardUri, string $cardData, AddressBook $book, CalendarInstance $calendarInstance): void\n    {\n        $objectUid = $book->getUri().'-'.$cardUri;\n        $objectUri = $objectUid.'.ics';\n        $calendarData = $this->buildDataFromContact($cardData);\n\n        $calendar = $calendarInstance->getCalendar();\n        // This is the structure that needs to be passed to the backend methods\n        $calendarId = [$calendar->getId(), $calendarInstance->getId()];\n\n        $existing = $this->doctrine->getRepository(CalendarObject::class)->findOneBy(['calendar' => $calendar, 'uri' => $objectUri]);\n\n        if (null === $calendarData) {\n            if (null !== $existing) {\n                $this->calendarBackend->deleteCalendarObject(\n                    [$calendar->getId(), $calendarInstance->getId()],\n                    $objectUri\n                );\n            }\n        } else {\n            if (null === $existing) {\n                $this->calendarBackend->createCalendarObject(\n                    [$calendar->getId(), $calendarInstance->getId()],\n                    $objectUri,\n                    $calendarData->serialize()\n                );\n            } else {\n                if ($this->birthdayEventChanged($existing->getCalendarData(), $calendarData)) {\n                    $this->calendarBackend->updateCalendarObject(\n                        [$calendar->getId(), $calendarInstance->getId()],\n                        $objectUri,\n                        $calendarData->serialize()\n                    );\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Services/IMAPAuth.php",
    "content": "<?php\n\nnamespace App\\Services;\n\nuse App\\Entity\\User;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Sabre\\DAV\\Auth\\Backend\\AbstractBasic;\nuse Webklex\\PHPIMAP\\Client;\nuse Webklex\\PHPIMAP\\ClientManager;\n\nfinal class IMAPAuth extends AbstractBasic\n{\n    /**\n     * Doctrine registry.\n     *\n     * @var ManagerRegistry\n     */\n    private $doctrine;\n\n    /**\n     * Utils class.\n     *\n     * @var Utils\n     */\n    private $utils;\n\n    /**\n     * Should we auto create the user upon successful\n     * login if it does not exist yet.\n     *\n     * @var bool\n     */\n    private $autoCreate;\n\n    /**\n     * IMAP server host.\n     *\n     * @var string\n     */\n    private $IMAPHost;\n\n    /**\n     * IMAP server port.\n     *\n     * @var int\n     */\n    private $IMAPPort;\n\n    /**\n     * IMAP encryption method. Could be ssl, tls or false.\n     *\n     * @var mixed (string or bool)\n     */\n    private $IMAPEncryptionMethod;\n\n    /**\n     * Should we validate the certificate?\n     *\n     * @var bool\n     */\n    private $IMAPCertificateValidation;\n\n    public function __construct(ManagerRegistry $doctrine, Utils $utils, string $IMAPAuthUrl, bool $autoCreate, string $IMAPEncryptionMethod, bool $IMAPCertificateValidation)\n    {\n        $components = parse_url($IMAPAuthUrl);\n\n        if (!$components) {\n            throw new Exception('IMAP Error (parsing IMAP url \"'.$IMAPAuthUrl.'\"): '.$e->getMessage());\n        }\n\n        $this->IMAPHost = $components['host'] ?? null;\n\n        // Trying to choose the best port if it was not provided,\n        // defaulting to 993 (secure)\n        if (isset($components['port'])) {\n            $this->IMAPPort = $components['port'];\n        } elseif (false === $this->IMAPEncryptionMethod) {\n            $this->IMAPPort = 143;\n        } else {\n            $this->IMAPPort = 993;\n        }\n\n        // We're making sure that only ssl, tls or 'false' are passed down to the IMAP client,\n        // defaulting to SSL\n        $IMAPEncryptionMethodCleaned = strtolower($IMAPEncryptionMethod);\n        if ('false' === $IMAPEncryptionMethodCleaned) {\n            $this->IMAPEncryptionMethod = false;\n        } elseif ('tls' === $IMAPEncryptionMethodCleaned) {\n            $this->IMAPEncryptionMethod = 'tls';\n        } else {\n            $this->IMAPEncryptionMethod = 'ssl';\n        }\n        $this->IMAPCertificateValidation = $IMAPCertificateValidation;\n\n        $this->autoCreate = $autoCreate;\n\n        $this->doctrine = $doctrine;\n        $this->utils = $utils;\n    }\n\n    /**\n     * Connects to an IMAP server and tries to authenticate.\n     * If the user does not exist, create it (depending on the autoCreate flag).\n     */\n    protected function imapOpen(string $username, string $password): bool\n    {\n        $cm = new ClientManager($options = []);\n\n        // Create a new instance of the IMAP client manually\n        $client = $cm->make([\n            'host' => $this->IMAPHost,\n            'port' => $this->IMAPPort,\n            'encryption' => $this->IMAPEncryptionMethod,\n            'validate_cert' => $this->IMAPCertificateValidation,\n            'username' => $username,\n            'password' => $password,\n            'protocol' => 'imap',\n        ]);\n\n        try {\n            $client->connect();\n            $client->disconnect();\n            $success = true;\n        } catch (\\Exception $e) {\n            error_log('IMAP Error (connection): '.$e->getMessage());\n            $success = false;\n        }\n\n        // Auto-create the user if it does not already exist in the database\n        if ($success && $this->autoCreate) {\n            $user = $this->doctrine->getRepository(User::class)->findOneBy(['username' => $username]);\n\n            if (!$user) {\n                // We only have a username, so we use it for displayname and email\n                $this->utils->createPasswordlessUserWithDefaultObjects($username, $username, $username);\n\n                $em = $this->doctrine->getManager();\n\n                try {\n                    $em->flush();\n                } catch (\\Exception $e) {\n                    error_log('IMAP Error (flush): '.$e->getMessage());\n                }\n            }\n        }\n\n        return $success;\n    }\n\n    /**\n     * Validates a username and password by trying to authenticate against IMAP.\n     */\n    protected function validateUserPass($username, $password): bool\n    {\n        return $this->imapOpen($username, $password);\n    }\n}\n"
  },
  {
    "path": "src/Services/LDAPAuth.php",
    "content": "<?php\n\nnamespace App\\Services;\n\nuse App\\Entity\\User;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Sabre\\DAV\\Auth\\Backend\\AbstractBasic;\n\nfinal class LDAPAuth extends AbstractBasic\n{\n    /**\n     * LDAP server uri.\n     * e.g. ldaps://ldap.example.org.\n     *\n     * @var string\n     */\n    private $LDAPAuthUrl;\n\n    /*\n     * LDAP dn pattern for binding\n     *\n     * %u   - gets replaced by full username\n     * %U   - gets replaced by user part when the\n     *        username is an email address\n     * %d   - gets replaced by domain part when the\n     *        username is an email address\n     * %1-9 - gets replaced by parts of the the domain\n     *        split by '.' in reverse order\n     *        mail.example.org: %1 = org, %2 = example, %3 = mail\n     *\n     * A common pattern is \"mail=%u\"\n     * @var string\n     */\n    private $LDAPDnPattern;\n\n    /*\n     * LDAP attribute used for mail\n     *\n     * @var string\n     */\n    private $LDAPMailAttribute;\n\n    /**\n     * Doctrine registry.\n     *\n     * @var ManagerRegistry\n     */\n    private $doctrine;\n\n    /**\n     * Utils class.\n     *\n     * @var Utils\n     */\n    private $utils;\n\n    /**\n     * Should we auto create the user upon successful\n     * login if it does not exist yet.\n     *\n     * @var bool\n     */\n    private $autoCreate;\n\n    /**\n     * Indicates what to do with certificate.\n     * see https://www.php.net/manual/en/ldap.constants.php#constant.ldap-opt-x-tls-require-cert.\n     */\n    private $LDAPCertificateCheckingStrategy;\n\n    /**\n     * Creates the backend object.\n     */\n    public function __construct(ManagerRegistry $doctrine, Utils $utils, string $LDAPAuthUrl, string $LDAPDnPattern, ?string $LDAPMailAttribute, bool $autoCreate, ?string $LDAPCertificateCheckingStrategy)\n    {\n        $this->LDAPAuthUrl = $LDAPAuthUrl;\n        $this->LDAPDnPattern = $LDAPDnPattern;\n        $this->LDAPMailAttribute = $LDAPMailAttribute ?? 'mail';\n        $this->autoCreate = $autoCreate;\n        $this->LDAPCertificateCheckingStrategy = $LDAPCertificateCheckingStrategy ?? 'try';\n\n        $this->doctrine = $doctrine;\n        $this->utils = $utils;\n    }\n\n    /**\n     * Connects to an LDAP server and tries to authenticate.\n     *\n     * @param string $username\n     * @param string $password\n     *\n     * @return bool\n     */\n    protected function ldapOpen($username, $password)\n    {\n        switch ($this->LDAPCertificateCheckingStrategy) {\n            case 'never':\n                $cert_strategy = LDAP_OPT_X_TLS_NEVER;\n                break;\n            case 'hard':\n                $cert_strategy = LDAP_OPT_X_TLS_HARD;\n                break;\n            case 'demand':\n                $cert_strategy = LDAP_OPT_X_TLS_DEMAND;\n                break;\n            case 'allow':\n                $cert_strategy = LDAP_OPT_X_TLS_ALLOW;\n                break;\n            case 'try':\n                $cert_strategy = LDAP_OPT_X_TLS_TRY;\n                break;\n            default:\n                error_log('Invalid certificate checking strategy: '.$this->LDAPCertificateCheckingStrategy);\n\n                return false;\n        }\n\n        if (false === ldap_set_option(null, LDAP_OPT_X_TLS_REQUIRE_CERT, $cert_strategy)) {\n            error_log('LDAP Error (ldap_set_option with '.$cert_strategy.'): failed');\n\n            return false;\n        }\n\n        try {\n            $ldap = ldap_connect($this->LDAPAuthUrl);\n        } catch (\\Exception $e) {\n            error_log('LDAP Error (ldap_connect with '.$this->LDAPAuthUrl.'): '.$e->getMessage());\n\n            return false;\n        }\n\n        if (false === $ldap) {\n            error_log('LDAP Error (ldap_connect with '.$this->LDAPAuthUrl.'): provided LDAP URI does not seems plausible');\n\n            return false;\n        }\n\n        if (!ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3)) {\n            error_log('LDAP Error (ldap_set_option): could not set LDAP_OPT_PROTOCOL_VERSION to 3');\n\n            return false;\n        }\n\n        // Extract user and domain from username (in the form user@domain.org)\n        $user_parts = explode('@', $username, 2);\n\n        $ldap_user = $user_parts[0];\n\n        if (count($user_parts) > 1) {\n            $ldap_domain = $user_parts[1];\n        } else {\n            $ldap_domain = '';\n        }\n\n        // Replace common placeholders\n        $dn = str_replace(['%u', '%U', '%d'], [$username, $ldap_user, $ldap_domain], $this->LDAPDnPattern);\n\n        // Replace domain parts\n        $domain_split = array_reverse(explode('.', $ldap_domain));\n        for ($i = 1; $i <= count($domain_split) and $i <= 9; ++$i) {\n            $dn = str_replace('%'.$i, $domain_split[$i - 1], $dn);\n        }\n\n        $success = false;\n        try {\n            $bind = ldap_bind($ldap, $dn, $password);\n            if ($bind) {\n                $success = true;\n            }\n        } catch (\\Exception $e) {\n            error_log('LDAP Error (ldap_bind to '.$this->LDAPAuthUrl.'): '.ldap_error($ldap).' ('.ldap_errno($ldap).')');\n        }\n\n        if ($success && $this->autoCreate) {\n            $user = $this->doctrine->getRepository(User::class)->findOneBy(['username' => $username]);\n\n            if (!$user) {\n                // Default fallback values\n                $displayName = $username;\n                $email = $username;\n\n                // Try to extract display name and email for this user.\n                // NB: We suppose display name is `cn` (email is configurable, generally `mail`)\n                try {\n                    $search_results = ldap_read($ldap, $dn, '(objectclass=*)', ['cn', $this->LDAPMailAttribute]);\n                } catch (\\Exception $e) {\n                    $search_results = false;\n                    // Probably a \"No such object\" error, ignore and use available credentials (username)\n                }\n\n                if (false !== $search_results) {\n                    $entry = ldap_get_entries($ldap, $search_results);\n\n                    if (false !== $entry) {\n                        if (!empty($entry[0]['cn'])) {\n                            $displayName = $entry[0]['cn'][0];\n                        }\n                        if (!empty($entry[0][$this->LDAPMailAttribute])) {\n                            $email = $entry[0][$this->LDAPMailAttribute][0];\n                        }\n                    }\n                }\n\n                $this->utils->createPasswordlessUserWithDefaultObjects($username, $displayName, $email);\n\n                $em = $this->doctrine->getManager();\n\n                try {\n                    $em->flush();\n                } catch (\\Exception $e) {\n                    error_log('LDAP Error (flush): '.$e->getMessage());\n                }\n            }\n        }\n\n        if (isset($ldap) && $ldap) {\n            ldap_close($ldap);\n        }\n\n        return $success;\n    }\n\n    /**\n     * Validates a username and password by trying to authenticate against LDAP.\n     *\n     * @param string $username\n     * @param string $password\n     */\n    protected function validateUserPass($username, $password): bool\n    {\n        return $this->ldapOpen($username, $password);\n    }\n}\n"
  },
  {
    "path": "src/Services/Utils.php",
    "content": "<?php\n\nnamespace App\\Services;\n\nuse App\\Entity\\AddressBook;\nuse App\\Entity\\Calendar;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\Principal;\nuse App\\Entity\\User;\nuse Doctrine\\Persistence\\ManagerRegistry;\nuse Symfony\\Contracts\\Translation\\TranslatorInterface;\n\nfinal class Utils\n{\n    /**\n     * Authentication realm.\n     *\n     * @var string\n     */\n    private $authRealm;\n\n    /**\n     * The translation service.\n     *\n     * @var TranslatorInterface\n     */\n    private $trans;\n\n    /**\n     * Doctrine registry.\n     *\n     * @var ManagerRegistry\n     */\n    private $doctrine;\n\n    public function __construct(ManagerRegistry $doctrine, TranslatorInterface $trans, ?string $authRealm)\n    {\n        $this->authRealm = $authRealm ?? User::DEFAULT_AUTH_REALM;\n        $this->trans = $trans;\n        $this->doctrine = $doctrine;\n    }\n\n    /**\n     * Hash a password according to the realm.\n     * Important note: It is very insecure and this is used only for the legacy sabre/dav implementation.\n     */\n    public function hashPassword(string $username, string $password): string\n    {\n        return md5($username.':'.$this->authRealm.':'.$password);\n    }\n\n    public function createPasswordlessUserWithDefaultObjects(string $username, string $displayName, string $email)\n    {\n        $user = new User();\n        $user->setUsername($username);\n\n        // Set the password to a random string (but hashed beforehand)\n        $randomBytes = substr(bin2hex(random_bytes(256)), 0, 48);\n        $hash = password_hash($randomBytes, PASSWORD_DEFAULT);\n        $user->setPassword($hash);\n\n        // Create principal, default calendar and addressbook\n        $principal = new Principal();\n        $principal->setUri(Principal::PREFIX.$username)\n                ->setDisplayName($displayName)\n                ->setEmail($email)\n                ->setIsAdmin(false);\n\n        $calendarInstance = new CalendarInstance();\n        $calendar = new Calendar();\n        $calendarInstance->setPrincipalUri(Principal::PREFIX.$username)\n                ->setUri('default') // No risk of collision since unicity is guaranteed by the new user principal\n                ->setDisplayName($this->trans->trans('default.calendar.title'))\n                ->setDescription($this->trans->trans('default.calendar.description', ['user' => $displayName]))\n                ->setCalendar($calendar);\n\n        // Enable delegation by default\n        $principalProxyRead = new Principal();\n        $principalProxyRead->setUri($principal->getUri().Principal::READ_PROXY_SUFFIX)\n                        ->setIsMain(false);\n\n        $principalProxyWrite = new Principal();\n        $principalProxyWrite->setUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX)\n                        ->setIsMain(false);\n\n        $addressbook = new AddressBook();\n        $addressbook->setPrincipalUri(Principal::PREFIX.$username)\n                ->setUri('default') // No risk of collision since unicity is guaranteed by the new user principal\n                ->setDisplayName($this->trans->trans('default.addressbook.title'))\n                ->setDescription($this->trans->trans('default.addressbook.description', ['user' => $displayName]));\n\n        // Persist all items\n        $em = $this->doctrine->getManager();\n        $em->persist($principalProxyRead);\n        $em->persist($principalProxyWrite);\n        $em->persist($calendarInstance);\n        $em->persist($addressbook);\n        $em->persist($principal);\n        $em->persist($user);\n    }\n}\n"
  },
  {
    "path": "src/Version.php",
    "content": "<?php\n\nnamespace App;\n\nfinal class Version\n{\n    public const VERSION = '5.4.1';\n}\n"
  },
  {
    "path": "templates/_partials/add_delegate_modal.html.twig",
    "content": "<div class=\"modal fade\" id=\"addDelegateModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n  <div class=\"modal-dialog\" role=\"document\">\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <h5 class=\"modal-title\" id=\"exampleModalLabel\">{{ \"calendars.delegates.new\"|trans }}</h5>\n        <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"{{ \"close\"|trans }}\"></button>\n      </div>\n      <div class=\"modal-body\">\n        <form>\n          <div class=\"form-group\">\n            <label for=\"member\" class=\"col-form-label\">{{ \"calendars.delegates.member\"|trans }}</label>\n            <select class=\"form-control\" id=\"addDelegateModal-member\">\n              {% for principal in principals %}\n                <option value=\"{{ principal.id}}\">{{ principal.displayName }} ({{ principal.email }})</option>\n              {% endfor %}\n            </select>\n            <small class=\"form-text text-muted\">{{ \"delegates.member.help\"|trans }}</small>\n          </div>\n          <div class=\"form-check form-switch mt-2\">\n            <input class=\"form-check-input\" type=\"checkbox\" value=\"\" id=\"addDelegateModal-writeAccess\">\n            <label class=\"form-check-label\" for=\"write\">\n              {{ \"calendars.delegates.write.give\"|trans }}\n            </label>\n          </div>\n        </form>\n      </div>\n      <div class=\"modal-footer\">\n        <button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">{{ \"cancel\"|trans }}</button>\n        <a data-href=\"{{ path('user_delegate_add', {userId: userId}) }}\" href=\"#\" class=\"btn btn-primary\" id=\"addDelegateModal-cta\">{{ \"add\"|trans }}</a>\n      </div>\n    </div>\n  </div>\n</div>"
  },
  {
    "path": "templates/_partials/back_button.html.twig",
    "content": "<div class=\"mb-3\"><a href=\"{{ url }}\" class=\"link-opacity-75 link-offset-2\">« {{ text }}</a></div>"
  },
  {
    "path": "templates/_partials/delegate_row.html.twig",
    "content": "<div class=\"list-group-item p-3\">\n    <div class=\"d-flex w-100 justify-content-between\">\n        <h5 class=\"me-auto\">\n            {{ delegate.displayName }}\n            <a href=\"mailto:{{ delegate.email }}\">&lsaquo;{{ delegate.email }}&rsaquo;</a>\n            {% if has_write %}\n                <span class=\"badge bg-success\">{{ \"delegates.write\"|trans }}</span>\n            {% else %}\n                <span class=\"badge bg-info\">{{ \"delegates.readonly\"|trans }}</span>\n            {% endif %}\n        </h5>\n        <div class=\"me-0 text-right d-lg-block d-none\">\n            <a href=\"#\" \n                data-bs-toggle=\"modal\" data-bs-target=\"#deleteModal-delegates\"\n                data-href=\"{{ path('user_delegate_remove',{userId: userId, principalProxyId: has_write ? principalProxyWrite.id : principalProxyRead.id, delegateId: delegate.id})}}\" \n                data-flavour=\"delegates\" \n                class=\"btn btn-sm btn-outline-danger ms-1 mb-1\"\n            >⚠&nbsp;{{ \"remove\"|trans }}</a>\n        </div>\n    </div>\n    <p class=\"mb-1\">{{ \"users.username\"|trans }} : <code>{{ delegate.username }}</code></p>\n    <small>{{ \"users.uri\"|trans }} : <code>{{ delegate.uri }}</code></small>\n    <div class=\"btn-group btn-group-sm mt-3 d-flex d-lg-none\" role=\"group\">\n        <a href=\"#\" data-href=\"{{ path('user_delegate_remove',{userId: userId, principalProxyId: has_write ? principalProxyWrite.id : principalProxyRead.id, delegateId: delegate.id})}}\" data-flavour=\"delegates\" class=\"btn btn-outline-danger flex-fill flex-shrink-1 delete-modal\"><span class=\"d-none d-sm-inline\">⚠&nbsp;</span>{{ \"remove\"|trans }}</a>\n    </div>\n</div>\n"
  },
  {
    "path": "templates/_partials/delete_modal.html.twig",
    "content": "<div id=\"deleteModal-{{ flavour }}\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" rel=\"deleteModal\">\n  <div class=\"modal-dialog modal-dialog-centered\" role=\"document\">\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <h5 class=\"modal-title\">{{ (flavour ~ \".modal.title\")|trans }}</h5>\n        <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"{{ \"close\"|trans }}\"></button>\n      </div>\n      <div class=\"modal-body\">\n        <p>{{ (flavour ~ \".modal.text\")|trans }}</p>\n      </div>\n      <div class=\"modal-footer\">\n        <button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">{{ \"cancel\"|trans }}</button>\n        <a href=\"#\" type=\"button\" class=\"btn btn-primary\" id=\"deleteModal-{{ flavour }}-cta\">{{ \"yes\"|trans }}</a>\n      </div>\n    </div>\n  </div>\n</div>"
  },
  {
    "path": "templates/_partials/flashes.html.twig",
    "content": "<div aria-live=\"polite\" aria-atomic=\"true\" class=\"flashes\">\n  <div class=\"inner\">\n    {% for label, messages in app.flashes %}\n      {% for message in messages %}\n        <div class=\"toast text-bg-{{ label }} border-0\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\">\n            <div class=\"d-flex\">\n              <div class=\"toast-body\">{{ message }}</div>\n              <button type=\"button\" class=\"btn-close btn-close-white me-2 m-auto\" data-bs-dismiss=\"toast\" aria-label=\"{{ \"close\"|trans }}\"></button>\n            </div>\n        </div>\n      {% endfor %}\n   {% endfor %}\n  </div>\n</div>\n"
  },
  {
    "path": "templates/_partials/navigation.html.twig",
    "content": "<nav class=\"navbar fixed-top navbar-expand-lg bg-body-tertiary\">\n  <div class=\"container\">\n    <a class=\"navbar-brand\" href=\"{{ path('dashboard') }}\">\n      <img src=\"/images/logo.png\" width=\"30\" height=\"30\" alt=\"\"> Davis\n    </a>\n    <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarNav\" aria-controls=\"navbarNav\" aria-expanded=\"false\" aria-label=\"{{ \"toggle.navigation\"|trans }}\">\n      <span class=\"navbar-toggler-icon\"></span>\n    </button>\n    <div class=\"collapse navbar-collapse\" id=\"navbarNav\">\n      <ul class=\"navbar-nav me-auto\">\n        <li class=\"nav-item{% if menu == \"dashboard\" %} active{% endif %}\">\n          <a class=\"nav-link\" href=\"{{ path('dashboard') }}\">{{ \"title.dashboard\"|trans }}</a>\n        </li>\n        <li class=\"nav-item{% if menu == \"resources\" %} active{% endif %}\">\n          <a class=\"nav-link\" href=\"{{ path('user_index') }}\">{{ \"title.users_and_resources\"|trans }}</a>\n        </li>\n      </ul>\n      {% if app.user %}\n      <ul class=\"navbar-nav\">\n        <li class=\"nav-item dropdown\">\n          <a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"navUserMenu\" role=\"button\" data-bs-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n            👤 {{ app.user.username }}\n          </a>\n          <div class=\"dropdown-menu\" aria-labelledby=\"navUserMenu\">\n            <a class=\"dropdown-item\" href=\"{{ path('app_logout') }}\">{{ \"logout\"|trans }}</a>\n          </div>\n        </li>\n        <li class=\"nav-item dropdown\">\n          <button class=\"nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center\" id=\"bd-theme\" type=\"button\" aria-expanded=\"false\" data-bs-toggle=\"dropdown\" aria-label=\"{{ \"toggle.theme\"|trans }}\">\n            <span class=\"theme-icon-active me-1\">☀️</span> <span class=\"d-lg-none ms-2\" id=\"bd-theme-text\">{{ \"toggle.theme\"|trans }}</span>\n          </button>\n          <ul class=\"dropdown-menu dropdown-menu-end\" aria-labelledby=\"bd-theme-text\" data-bs-popper=\"static\">\n            <li>\n              <button type=\"button\" class=\"dropdown-item d-flex align-items-center\" data-bs-theme-value=\"light\" aria-pressed=\"true\">\n                <span class=\"theme-icon me-2\">☀️</span> {{ \"theme.light\"|trans }}\n              </button>\n            </li>\n            <li>\n              <button type=\"button\" class=\"dropdown-item d-flex align-items-center\" data-bs-theme-value=\"dark\" aria-pressed=\"false\">\n                <span class=\"theme-icon me-2\">🌙</span> {{ \"theme.dark\"|trans }}\n              </button>\n            </li>\n            <li>\n              <button type=\"button\" class=\"dropdown-item d-flex align-items-center\" data-bs-theme-value=\"auto\" aria-pressed=\"false\">\n                <span class=\"theme-icon me-2\">🔘</span> {{ \"theme.auto\"|trans }}\n              </button>\n            </li>\n          </ul>\n        </li>\n      </ul>\n      {% endif %}\n    </div>\n  </div>\n</nav>"
  },
  {
    "path": "templates/_partials/share_modal.html.twig",
    "content": "<div class=\"modal fade\" id=\"shareModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n  <div class=\"modal-dialog\" role=\"document\">\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <h5 class=\"modal-title\" id=\"exampleModalLabel\">{{ \"calendars.sharing\"|trans }}</h5>\n        <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"{{ \"close\"|trans }}\"></button>\n      </div>\n      <div class=\"modal-body\">\n        <h4>{{ \"calendars.delegates.existing\"|trans }}</h4>\n        <template id=\"shareModal-shareeTemplate\">\n            <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n                <div class=\"d-flex align-items-center\">\n                    <span class=\"name\"></span>\n                    <span class=\"badge bg-info rounded-pill ms-2\"></span>\n                </div>\n                <a href=\"#\" class=\"btn btn-sm btn-danger revoke\">{{ \"revoke\"|trans }}</a>\n            </div>\n        </template>\n        <span class=\"d-none\" id=\"shareModal-none\"><em>{{ \"calendars.delegates.none\"|trans }}</em></span>\n        <ul class=\"list-group\" id=\"shareModal-shares\"></ul>\n        <form class=\"mt-2\">\n          <div class=\"form-group\">\n            <label for=\"member\" class=\"col-form-label\">{{ \"calendars.delegates.member.add\"|trans }}</label>\n            <select class=\"form-control\" id=\"shareModal-member\" {% if principals|length == 0 %}disabled=\"disabled\"{% endif %}>\n              {% for principal in principals %}\n                <option value=\"{{ principal.id}}\">{{ principal.displayName }} ({{ principal.email }})</option>\n              {% endfor %}\n            </select>\n            <small class=\"form-text text-muted\">\n            {% if principals|length == 0 %}\n              {{ \"calendars.delegates.member.none\"|trans }}\n            {% else %}\n              {{ \"calendars.delegates.member.help\"|trans }}\n            {% endif %}\n            </small>\n          </div>\n          <div class=\"form-check form-switch mt-2\">\n            <input class=\"form-check-input\" type=\"checkbox\" role=\"switch\" value=\"\" id=\"shareModal-writeAccess\" {% if principals|length == 0 %}disabled=\"disabled\"{% endif %}>\n            <label class=\"form-check-label\" for=\"write\">\n              {{ \"calendars.delegates.write.give\"|trans }}\n            </label>\n          </div>\n        </form>\n      </div>\n      <div class=\"modal-footer\">\n        <button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">{{ \"cancel\"|trans }}</button>\n        <a href=\"#\" id=\"shareModal-addSharee\" class=\"btn btn-primary{% if principals|length == 0 %} disabled{% endif %}\">{{ \"add\"|trans }}</a>\n      </div>\n    </div>\n  </div>\n</div>"
  },
  {
    "path": "templates/addressbooks/edit.html.twig",
    "content": "{% extends 'base.html.twig' %}\n{% set menu = 'resources' %}\n\n{% block body %}\n\n{% include '_partials/back_button.html.twig' with { url: path('addressbook_index', {userId: userId}), text: \"addressbooks.back\"|trans({'user': principal.displayName }) } %}\n\n{% if addressbook.id %}\n<h1 class=\"display-4 fw-lighter mb-5\">{{ \"addressbooks.edit\"|trans({'name': addressbook.displayName }) }}</h1>\n{% else %}\n<h1 class=\"display-4 fw-lighter mb-5\">{{ \"addressbooks.new\"|trans }} <small class=\"text-muted\">for {{ principal.displayName }}</small></h1>\n{% endif %}\n\n{{ form(form) }}\n\n{% endblock %}"
  },
  {
    "path": "templates/addressbooks/index.html.twig",
    "content": "{% extends 'base.html.twig' %}\n{% set menu = 'resources' %}\n\n{% block body %}\n\n{% include '_partials/back_button.html.twig' with { url: path('user_index'), text: \"users.back\"|trans } %}\n\n<h1 class=\"display-4 d-flex fw-lighter justify-content-between\">{{ \"addressbooks.for\"|trans({'who': principal.displayName}) }} <a href=\"{{ path('addressbook_create', {userId: userId}) }}\" class=\"btn btn-sm btn-success mb-auto mt-auto\">+ {{ \"addressbooks.new\"|trans }}</a></h1>\n\n<div class=\"list-group mt-4\">\n{% for addressbook in addressbooks %}\n    <div class=\"list-group-item p-3\">\n        <div class=\"d-flex w-100 justify-content-between\">\n            <h5 class=\"mb-1 me-auto\">{{ addressbook.displayName }}</h5>\n            <div class=\"me-0 text-right d-md-block d-none\">\n                <a href=\"{{ path('addressbook_edit',{userId: userId, id: addressbook.id})}}\" class=\"btn btn-sm btn-outline-primary ms-1\">✎ {{ \"edit\"|trans }}</a>\n                <a href=\"#\"\n                    data-bs-toggle=\"modal\" data-bs-target=\"#deleteModal-addressbooks\"\n                    data-href=\"{{ path('addressbook_delete',{userId: userId, id: addressbook.id})}}\"\n                    class=\"btn btn-sm btn-outline-danger ms-1\" \n                    data-flavour=\"addressbooks\"\n                >⚠ {{ \"delete\"|trans }}</a>\n            </div>\n        </div>\n        <p class=\"mb-1\">{{ addressbook.description }}</p>\n        <small>{{ \"addressbooks.uri\"|trans }} : <code>{{ addressbook.uri }}</code> — {{ \"addressbooks.contacts\"|trans({'%count%': addressbook.cards|length}) }}</small>\n        <div class=\"btn-group btn-group-sm mt-3 d-flex d-md-none\" role=\"group\">\n            <a href=\"{{ path('addressbook_edit',{userId: userId, id: addressbook.id})}}\" class=\"btn btn-sm btn-outline-primary\">✎ {{ \"edit\"|trans }}</a>\n            <a href=\"#\"\n                data-bs-toggle=\"modal\" data-bs-target=\"#deleteModal-addressbooks\"\n                data-href=\"{{ path('addressbook_delete',{userId: userId, id: addressbook.id})}}\" \n                class=\"btn btn-sm btn-outline-danger\"\n                data-flavour=\"addressbooks\"\n            >⚠ {{ \"delete\"|trans }}</a>\n        </div>\n    </div>\n{% endfor %}\n</div>\n\n{% include '_partials/delete_modal.html.twig' with {flavour: 'addressbooks'} %}\n\n{% endblock %}"
  },
  {
    "path": "templates/base.html.twig",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"UTF-8\">\n        <title>{% block title %}Davis{% endblock %}</title>\n        <script type=\"text/javascript\" src=\"/js/color.mode.toggler.js\"></script>\n        <link rel=\"stylesheet\" href=\"/css/bootstrap.min.css\" />\n        <link rel=\"stylesheet\" type=\"text/css\" href=\"/css/style.css\">\n    </head>\n    <body>\n        {% include '_partials/navigation.html.twig' %}\n        {% include '_partials/flashes.html.twig' %}\n        <div class=\"container\">\n            {% block body %}{% endblock %}\n        </div>\n        <script type=\"text/javascript\" src=\"/js/bootstrap.bundle.min.js\"></script>\n        <script type=\"text/javascript\" src=\"/js/app.js\"></script>\n    </body>\n</html>\n"
  },
  {
    "path": "templates/calendars/edit.html.twig",
    "content": "{% extends 'base.html.twig' %}\n{% set menu = 'resources' %}\n\n{% block body %}\n\n{% include '_partials/back_button.html.twig' with { url: path('calendar_index', {userId: userId}), text: \"calendars.back\"|trans({'user': principal.displayName }) } %}\n\n{% if calendar.id %}\n<h1 class=\"display-4 fw-lighter mb-5\">{{ \"calendars.edit\"|trans({'name': calendar.displayName }) }}</h1>\n{% else %}\n<h1 class=\"display-4 fw-lighter mb-5\">{{ \"calendars.new\"|trans }} <small class=\"text-muted\">for {{ principal.displayName }}</small></h1>\n{% endif %}\n\n{{ form(form) }}\n\n{% endblock %}"
  },
  {
    "path": "templates/calendars/index.html.twig",
    "content": "{% extends 'base.html.twig' %}\n{% set menu = 'resources' %}\n\n{% block body %}\n\n{% include '_partials/back_button.html.twig' with { url: path('user_index'), text: \"users.back\"|trans } %}\n\n<h1 class=\"display-4 fw-lighter d-flex justify-content-between\">{{ \"calendars.for\"|trans({'who': principal.displayName}) }} <a href=\"{{ path('calendar_create', {userId: userId}) }}\" class=\"btn btn-sm btn-success mb-auto mt-auto\">+ {{ \"calendars.new\"|trans }}</a></h1>\n\n<div class=\"list-group mt-4\">\n{% for compoundObject in calendars %}\n    {% set calendar = compoundObject.entity %}\n    {% set davUri = compoundObject.uri %}\n    <div class=\"list-group-item p-3\">\n        <div class=\"d-flex w-100 justify-content-between\">\n            <h5 class=\"mb-1 me-auto\">\n                {{ calendar.displayName }}\n                {% if calendar.isPublic() %}\n                    <span class=\"badge bg-success ml-1\">{{ ('calendar.public')|trans }}</span>\n                {% endif %}\n                <a href=\"#\" tabindex=\"0\" class=\"badge badge-indicator\" role=\"button\" data-bs-toggle=\"popover\" data-bs-title=\"{{ 'calendars.setup.title'|trans }}\" data-bs-html='true' data-bs-content=\"URI: <code>{{ calendar.uri }}</code><br />Absolute path: <code>{{ davUri }}</code>\">ⓘ</a>\n                <span class=\"badge badge-indicator\" style=\"background-color: {{ calendar.calendarColor }}\">&nbsp;</span>\n            </h5>\n            <div class=\"me-0 text-right d-md-block d-none\">\n                {% if not calendar.isPublic() %}\n                    <a href=\"#\"\n                        data-bs-toggle=\"modal\" data-bs-target=\"#shareModal\"\n                        data-sharees-href=\"{{ path('calendar_shares',{userId: userId, calendarid: calendar.calendar.id})}}\" \n                        data-href=\"{{ path('calendar_share_add', {userId: userId, instanceid: calendar.id}) }}\" \n                        class=\"btn btn-sm btn-outline-info ms-1 share-modal\"\n                    >🔗 {{ \"sharing\"|trans }}</a>\n                {% endif %}\n                <a href=\"{{ path('calendar_edit',{userId: userId, id: calendar.id})}}\" class=\"btn btn-sm btn-outline-primary ms-1\">✎ {{ \"edit\"|trans }}</a>\n                <a href=\"#\" \n                    data-bs-toggle=\"modal\" data-bs-target=\"#deleteModal-calendars\"\n                    data-href=\"{{ path('calendar_delete',{userId: userId, id: calendar.id})}}\"\n                    data-flavour=\"calendars\"\n                    class=\"btn btn-sm btn-outline-danger ms-1 delete-modal\"\n                >⚠ {{ \"delete\"|trans }}</a>\n            </div>\n        </div>\n        <p class=\"mb-1\">{{ calendar.description }}</p>\n        <small>{% if calendar.calendar.components|split(',')|length > 0 %}\n            {% if constant('\\\\App\\\\Entity\\\\Calendar::COMPONENT_EVENTS') in calendar.calendar.components %}<span class=\"badge bg-primary\">{{ \"calendars.component.events\"|trans }}</span>{% endif %}\n            {% if constant('\\\\App\\\\Entity\\\\Calendar::COMPONENT_NOTES') in calendar.calendar.components %}<span class=\"badge bg-info\">{{ \"calendars.component.notes\"|trans }}</span>{% endif %}\n            {% if constant('\\\\App\\\\Entity\\\\Calendar::COMPONENT_TODOS') in calendar.calendar.components %}<span class=\"badge bg-primary-subtle\">{{ \"calendars.component.todos\"|trans }}</span>{% endif %}\n        {% endif %}\n         — {{ \"calendars.entries\"|trans({'%count%': calendar.calendar.objects|length}) }}\n        </small>\n        <div class=\"btn-group btn-group-sm mt-3 d-flex d-md-none\" role=\"group\">\n            {% if not calendar.isPublic() %}\n                <a href=\"#\" \n                    data-bs-toggle=\"modal\" data-bs-target=\"#shareModal\"\n                    data-sharees-href=\"{{ path('calendar_shares',{userId: userId, calendarid: calendar.calendar.id})}}\" \n                    data-href=\"{{ path('calendar_share_add', {userId: userId, instanceid: calendar.id}) }}\" \n                    class=\"btn btn-outline-info share-modal\"\n                >🔗 {{ \"sharing\"|trans }}</a>\n            {% endif %}\n            <a href=\"{{ path('calendar_edit',{userId: userId, id: calendar.id})}}\" class=\"btn btn-outline-primary\">✎ {{ \"edit\"|trans }}</a>\n            <a href=\"#\" \n                data-bs-toggle=\"modal\" data-bs-target=\"#deleteModal-calendars\"\n                data-href=\"{{ path('calendar_delete',{userId: userId, id: calendar.id})}}\"\n                data-flavour=\"calendars\"\n                class=\"btn btn-outline-danger delete-modal\"\n            >⚠ {{ \"delete\"|trans }}</a>\n        </div>\n    </div>\n{% endfor %}\n</div>\n\n{% if shared|length > 0 %}\n    <h2 class=\"h4 mt-5 fw-lighter\">{{ \"calendars.shared.with\"|trans({'who': principal.displayName}) }}</h2>\n\n    <div class=\"list-group mt-3\">\n    {% for compoundObject in shared %}\n        {% set calendar = compoundObject.entity %}\n        {% set davUri = compoundObject.uri %}\n        <div class=\"list-group-item p-3\">\n            <div class=\"d-flex w-100 justify-content-between\">\n                <h5 class=\"mb-1 me-auto\">\n                    {{ calendar.displayName }}\n                    {% if calendar.access == constant('Sabre\\\\DAV\\\\Sharing\\\\Plugin::ACCESS_READWRITE') %}\n                        <span class=\"badge bg-success ms-1\">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>\n                    {% else %}\n                        <span class=\"badge bg-info ms-1\">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>\n                    {% endif %}\n                    <a href=\"#\" tabindex=\"0\" class=\"badge badge-indicator ms-1\" role=\"button\" data-bs-toggle=\"popover\" data-bs-title=\"{{ 'calendars.setup.title'|trans }}\" data-bs-html='true' data-bs-content=\"URI: <code>{{ calendar.uri }}</code><br />Absolute path: <code>{{ davUri }}</code>\">ⓘ</a>\n                    <span class=\"badge badge-indicator\" style=\"background-color: {{ calendar.calendarColor }}\">&nbsp;</span>\n                </h5>\n                <div class=\"me-0 text-right d-md-block d-none\">\n                    <a href=\"{{ path('calendar_edit',{userId: userId, id: calendar.id})}}\" class=\"btn btn-sm btn-outline-primary ms-1\">✎ {{ \"edit\"|trans }}</a>\n                    <a href=\"#\"\n                        data-bs-toggle=\"modal\" data-bs-target=\"#deleteModal-revoke\"\n                        data-href=\"{{ path('calendar_revoke',{userId: userId, id: calendar.id})}}\"\n                        data-flavour=\"revoke\"\n                        class=\"btn btn-sm btn-outline-danger ms-1\"\n                    >🚫 {{ \"revoke\"|trans }}</a>\n                </div>\n            </div>\n            <p class=\"mb-1\">{{ calendar.description }}</p>\n            <small>{% if calendar.calendar.components|split(',')|length > 0 %}\n                {% if constant('\\\\App\\\\Entity\\\\Calendar::COMPONENT_EVENTS') in calendar.calendar.components %}<span class=\"badge bg-primary\">{{ \"calendars.component.events\"|trans }}</span>{% endif %}\n                {% if constant('\\\\App\\\\Entity\\\\Calendar::COMPONENT_NOTES') in calendar.calendar.components %}<span class=\"badge bg-info\">{{ \"calendars.component.notes\"|trans }}</span>{% endif %}\n                {% if constant('\\\\App\\\\Entity\\\\Calendar::COMPONENT_TODOS') in calendar.calendar.components %}<span class=\"badge bg-primary-subtle\">{{ \"calendars.component.todos\"|trans }}</span>{% endif %}\n            {% endif %}\n            — {{ \"calendars.entries\"|trans({'%count%': calendar.calendar.objects|length}) }}\n            </small>\n            <div class=\"btn-group btn-group-sm mt-3 d-flex d-md-none\" role=\"group\">\n                <a href=\"{{ path('calendar_edit',{userId: userId, id: calendar.id})}}\" class=\"btn btn-outline-primary\">✎ {{ \"edit\"|trans }}</a>\n                <a href=\"#\" \n                    data-bs-toggle=\"modal\" data-bs-target=\"#deleteModal-revoke\"\n                    data-href=\"{{ path('calendar_revoke',{userId: userId, id: calendar.id})}}\" \n                    data-flavour=\"revoke\"\n                    class=\"btn btn-outline-danger\"\n                >🚫 {{ \"revoke\"|trans }}</a>\n            </div>\n        </div>\n    {% endfor %}\n    </div>\n\n    {% include '_partials/delete_modal.html.twig' with {flavour: 'revoke'} %}\n{% endif %}\n\n\n{% if auto|length > 0 %}\n    <h2 class=\"h4 mt-5 fw-lighter\">{{ \"calendars.auto\"|trans }}</h2>\n\n    <div class=\"list-group mt-3\">\n    {% for compoundObject in auto %}\n        {% set calendar = compoundObject.entity %}\n        {% set davUri = compoundObject.uri %}\n        <div class=\"list-group-item p-3\">\n            <div class=\"d-flex w-100 justify-content-between\">\n                <h5 class=\"mb-1 me-auto\">\n                    {{ calendar.displayName }}\n                    <span class=\"badge bg-warning ms-1\">{{ ('calendar.auto')|trans }}</span>\n                    <a href=\"#\" tabindex=\"0\" class=\"badge badge-indicator ms-1\" role=\"button\" data-bs-toggle=\"popover\" data-bs-title=\"{{ 'calendars.setup.title'|trans }}\" data-bs-html='true' data-bs-content=\"URI: <code>{{ calendar.uri }}</code><br />Absolute path: <code>{{ davUri }}</code>\">ⓘ</a>\n                    <span class=\"badge badge-indicator\" style=\"background-color: {{ calendar.calendarColor }}\">&nbsp;</span>\n                </h5>\n                <div class=\"me-0 text-right d-md-block d-none\">\n                    <a href=\"{{ path('calendar_edit',{userId: userId, id: calendar.id})}}\" class=\"btn btn-sm btn-outline-primary ms-1\">✎ {{ \"edit\"|trans }}</a>\n                </div>\n            </div>\n            <p class=\"mb-1\">{{ calendar.description }}</p>\n            <small>{% if calendar.calendar.components|split(',')|length > 0 %}\n                {% if constant('\\\\App\\\\Entity\\\\Calendar::COMPONENT_EVENTS') in calendar.calendar.components %}<span class=\"badge bg-primary\">{{ \"calendars.component.events\"|trans }}</span>{% endif %}\n                {% if constant('\\\\App\\\\Entity\\\\Calendar::COMPONENT_NOTES') in calendar.calendar.components %}<span class=\"badge bg-info\">{{ \"calendars.component.notes\"|trans }}</span>{% endif %}\n                {% if constant('\\\\App\\\\Entity\\\\Calendar::COMPONENT_TODOS') in calendar.calendar.components %}<span class=\"badge bg-primary-subtle\">{{ \"calendars.component.todos\"|trans }}</span>{% endif %}\n            {% endif %}\n            — {{ \"calendars.entries\"|trans({'%count%': calendar.calendar.objects|length}) }}\n            </small>\n            <div class=\"btn-group btn-group-sm mt-3 d-flex d-md-none\" role=\"group\">\n                <a href=\"{{ path('calendar_edit',{userId: userId, id: calendar.id})}}\" class=\"btn btn-outline-primary\">✎ {{ \"edit\"|trans }}</a>\n            </div>\n        </div>\n    {% endfor %}\n    </div>\n{% endif %}\n\n{% if subscriptions|length > 0 %}\n    <h2 class=\"h4 mt-5 fw-lighter\">{{ \"calendars.subscriptions\"|trans }}</h2>\n\n    <div class=\"list-group mt-3\">\n    {% for subscription in subscriptions %}\n        <div class=\"list-group-item p-3\">\n            <div class=\"d-flex w-100 justify-content-between\">\n                <h5 class=\"mb-1 me-auto\">\n                    {{ subscription.displayName }}\n                    <span class=\"badge bg-info ms-1\">{{ ('calendar.subscription')|trans }}</span>\n                    <a href=\"#\" tabindex=\"0\" class=\"badge badge-indicator ms-1\" role=\"button\" data-bs-toggle=\"popover\" data-bs-title=\"{{ 'calendars.setup.title'|trans }}\" data-bs-html='true' data-bs-content=\"URI: <code>{{ subscription.uri }}</code>\">ⓘ</a>\n                    <span class=\"badge badge-indicator\" style=\"background-color: {{ subscription.calendarColor }}\">&nbsp;</span>\n                </h5>\n            </div>\n            <code class=\"mb-1\">{{ subscription.source }}</code>\n        </div>\n    {% endfor %}\n    </div>\n{% endif %}\n\n\n{% include '_partials/share_modal.html.twig' with {principals: allPrincipals} %}\n{% include '_partials/delete_modal.html.twig' with {flavour: 'calendars'} %}\n\n{% endblock %}\n"
  },
  {
    "path": "templates/dashboard.html.twig",
    "content": "{% extends 'base.html.twig' %}\n{% set menu = 'dashboard' %}\n\n{% block body %}\n\n<h1 class=\"display-4 fw-lighter\">{{ \"title.dashboard\"|trans }}</h1>\n\n<div class=\"row\">\n  <div class=\"col-md\">\n    <h3 class=\"mb-3 mt-4 capabilities\">{{ \"dashboard.capabilities\"|trans }}</h3>\n\n    <ul class=\"list-group\">\n      {% if calDAVEnabled %}\n      <li class=\"list-group-item d-flex justify-content-between align-items-center list-group-item-success\">CalDAV\n        <span class=\"badge bg-success rounded-pill\">{{ \"enabled\"|trans }}</span></li>\n      {% else %}\n      <li class=\"list-group-item d-flex justify-content-between align-items-center list-group-item-danger\">CalDAV\n        <span class=\"badge bg-danger rounded-pill\">{{ \"disabled\"|trans }}</span></li>\n      {% endif %}\n\n      {% if cardDAVEnabled %}\n      <li class=\"list-group-item d-flex justify-content-between align-items-center list-group-item-success\">CardDAV\n        <span class=\"badge bg-success rounded-pill\">{{ \"enabled\"|trans }}</span></li>\n      {% else %}\n      <li class=\"list-group-item d-flex justify-content-between align-items-center list-group-item-danger\">CardDAV\n        <span class=\"badge bg-danger rounded-pill\">{{ \"disabled\"|trans }}</span></li>\n      {% endif %}\n\n      {% if webDAVEnabled %}\n      <li class=\"list-group-item d-flex justify-content-between align-items-center list-group-item-success\">WebDAV\n        <span class=\"badge bg-success rounded-pill\">{{ \"enabled\"|trans }}</span></li>\n      {% else %}\n      <li class=\"list-group-item d-flex justify-content-between align-items-center list-group-item-danger\">WebDAV\n        <span class=\"badge bg-danger rounded-pill\">{{ \"disabled\"|trans }}</span></li>\n      {% endif %}\n    </ul>\n\n    <h3 class=\"mb-3 mt-4 environment\">{{ \"dashboard.env\"|trans }}</h3>\n\n    <ul class=\"list-group\">\n      <li class=\"list-group-item list-group-item-primary d-flex justify-content-between\">\n        <span>{{ \"dashboard.version\"|trans }} : <code>{{ version }}</code> (SabreDAV <code>{{ sabredav_version }}</code>)</span>\n        <a class=\"github-link\" href=\"https://github.com/tchapi/davis\" data-bs-toggle=\"popover\" data-bs-trigger=\"hover\" data-bs-content=\"Project Github page\" data-bs-placement=\"bottom\"></a>\n      </li>\n      <li class=\"list-group-item list-group-item-secondary\">{{ \"dashboard.auth\"|trans }} : <code>{{ authMethod }}</code>{% if authMethod == 'Basic' %} ({{ \"dashboard.auth_realm\"|trans }}: <code>{{ authRealm }}</code>){% endif %}</li>\n      <li class=\"list-group-item list-group-item-secondary\">{{ \"dashboard.invite_from_address\"|trans }} : <code>{{ invite_from_address|default('Not set') }}</code></li>\n      <li class=\"list-group-item list-group-item-secondary d-flex justify-content-between align-items-center \">\n        <span>{{ \"dashboard.server_timezone\"|trans }} : <code>{{ timezone.actual_default }}</code></span>\n        {% if timezone.not_set_in_app %}<span class=\"badge bg-secondary rounded-pill\">{{ \"dashboard.no_timezone_configuration\"|trans }}</span>{% endif %}\n        {% if timezone.bad_value %}<span class=\"badge bg-danger rounded-pill\">{{ \"dashboard.bad_timezone_configuration\"|trans }}</span>{% endif %}\n      </li>\n    </ul>\n  </div>\n\n  <div class=\"col-md\">\n    <h3 class=\"mb-3 mt-4 objects\">{{ \"dashboard.objects\"|trans }}</h3>\n\n    <ul class=\"list-group mb-5\">\n      <li class=\"list-group-item d-flex justify-content-between align-items-center\">\n        {{ \"dashboard.users\"|trans }} <span class=\"badge bg-primary rounded-pill\">{{ users|length }}</span>\n      </li>\n      <li class=\"list-group-item d-flex justify-content-between align-items-center\">\n        <div>{{ \"dashboard.calendars\"|trans }}&nbsp;<span class=\"small text-muted ms-1 me-auto\">{{ \"dashboard.calendars.help\"|trans }}</span></div> <span class=\"badge bg-primary rounded-pill\">{{ calendars|length }}</span>\n      </li>\n      <li class=\"list-group-item d-flex justify-content-between align-items-center\">\n        ↳ {{ \"dashboard.events\"|trans }} <span class=\"badge bg-secondary rounded-pill\">{{ events|length }}</span>\n      </li>\n      <li class=\"list-group-item d-flex justify-content-between align-items-center\">\n        {{ \"dashboard.address_books\"|trans }} <span class=\"badge bg-primary rounded-pill\">{{ addressbooks|length }}</span>\n      </li>\n      <li class=\"list-group-item d-flex justify-content-between align-items-center\">\n        ↳ {{ \"dashboard.contacts\"|trans }} <span class=\"badge bg-secondary rounded-pill\">{{ contacts|length }}</span>\n      </li>\n    </ul>\n  </div>\n</div>\n\n{% endblock %}"
  },
  {
    "path": "templates/index.html.twig",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"UTF-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n        <title>{% block title %}Davis{% endblock %}</title>\n        <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\">\n        <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\">\n        <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\">\n        <link rel=\"manifest\" href=\"/site.webmanifest\">\n        <link rel=\"stylesheet\" href=\"/css/bootstrap.min.css\" />\n        <style type=\"text/css\">\n          .hero {\n            height: 100vh;\n            overflow: hidden;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            flex-direction: column;\n          }\n          .hero .list-group {\n            width: 240px;\n          }\n        </style>\n        <script type=\"text/javascript\" src=\"/js/color.mode.toggler.js\"></script>\n    </head>\n    <body>\n      <div class=\"hero\">\n        <img class=\"mb-2\" src=\"/images/logo.png\" width=\"60px\">\n        <h3 class=\"mb-4\">{{ \"davis\"|trans }}</h3>\n        <ul class=\"list-group\">\n          <li class=\"caldav list-group-item d-flex justify-content-between align-items-center\">\n            CalDAV\n            {% if calDAVEnabled %}<span class=\"badge bg-success rounded-pill\">{{ \"enabled\"|trans }}</span>{% else %}<span class=\"badge bg-danger rounded-pill\">{{ \"disabled\"|trans }}</span>{% endif %}\n          </li>\n          <li class=\"carddav list-group-item d-flex justify-content-between align-items-center\">\n            CardDAV\n            {% if cardDAVEnabled %}<span class=\"badge bg-success rounded-pill\">{{ \"enabled\"|trans }}</span>{% else %}<span class=\"badge bg-danger rounded-pill\">{{ \"disabled\"|trans }}</span>{% endif %}\n          </li>\n          <li class=\"webdav list-group-item d-flex justify-content-between align-items-center\">\n            WebDAV\n            {% if webDAVEnabled %}<span class=\"badge bg-success rounded-pill\">{{ \"enabled\"|trans }}</span>{% else %}<span class=\"badge bg-danger rounded-pill\">{{ \"disabled\"|trans }}</span>{% endif %}\n          </li>\n        </ul>\n        <a href=\"{{ path('dashboard') }}\" class=\"btn btn-sm btn-outline-secondary mt-4\">{{ \"admin.interface\"|trans }}</a>\n      </div>\n    </body>\n</html>\n"
  },
  {
    "path": "templates/mails/scheduling.html.twig",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>Calendar notification from {{ senderName }}</title>\n  <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n</head>\n<body leftmargin=\"0\" marginwidth=\"0\" topmargin=\"0\" marginheight=\"0\" offset=\"0\" style=\"margin: 0; padding: 0; background: #fff; color: #111111; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px;\">\n<table cellspacing=\"0\" cellpadding=\"0\" width=\"100%\">\n  <thead>\n    <tr>\n      <td colspan=\"3\"> </td>\n    </tr>\n    <tr>\n      <td width=\"*\"> </td>\n      <td width=\"500\">\n        <table width=\"100%\">\n          <tr>\n            <td width=\"1\" style=\"font-size: 64px; font-weight:bold;\">{{ dateTime|date('j') }}</td>\n            <td width=\"*\" style=\"padding: 10px 0;\">\n              <span style=\" font-size: 12px; color: #555555\">{{ dateTime|date('l') }}</span><br />\n              {{ dateTime|date('F') }}\n            </td>\n            <td width=\"58\" style=\"text-align: right;\">\n            </td>\n          </tr>\n        </table>\n      </td>\n      <td width=\"*\"> </td>\n    </tr>\n    <tr>\n      <td> </td>\n      <td style=\"border-top: 1px solid #cccccc\"> </td>\n      <td> </td>\n    </tr>\n  </thead>\n\n  <tbody>\n    <tr>\n      <td> </td>\n      <td style=\"text-align: center; padding: 10px 0;\">\n        {% if action == 'REQUEST' %}\n          <strong>{{ senderName }}</strong> invited you to\n          <h1 style=\"margin: 0; color: #2c7ea9; font-weight: 300; line-height: 36px;\">{{ summary }}</h1>\n        {% elseif action == 'CANCEL' %}\n          <h1 style=\"margin: 0; color: #2c7ea9; font-weight: 300; line-height: 36px;\">{{ summary }}</h1> has been canceled.\n        {% elseif action == 'ACCEPTED' %}\n          <strong>{{ senderName }}</strong> accepted your invitation to\n          <h1 style=\"margin: 0; color: #2c7ea9; font-weight: 300; line-height: 36px;\">{{ summary }}</h1>\n        {% elseif action == 'TENTATIVE' %}\n          <strong>{{ senderName }}</strong> tentatively accepted your invitation to\n          <h1 style=\"margin: 0; color: #2c7ea9; font-weight: 300; line-height: 36px;\">{{ summary }}</h1>\n        {% elseif action == 'DECLINED' %}\n          <strong>{{ senderName }}</strong> declined your invitation to\n          <h1 style=\"margin: 0; color: #2c7ea9; font-weight: 300; line-height: 36px;\">{{ summary }}</h1>\n        {% endif %}\n      </td>\n      <td> </td>\n    </tr>\n    <tr>\n      <td colspan=\"3\"> </td>\n    </tr>\n    <tr>\n      <td> </td>\n      <td style=\"border-top: 1px solid #cccccc\"> </td>\n      <td> </td>\n    </tr>\n    <tr>\n      <td> </td>\n      <td>\n        <table border=\"0\" cellpadding=\"0\" cellspacing=\"10\" width=\"100%\">\n          <tr>\n            <td style=\"vertical-align: bottom; color: #777777;\" width=\"100px\">When?</td>\n            <td>\n              <strong>{{ dateTime|date(\"l, F j\\\\<\\\\s\\\\u\\\\p\\\\>S\\\\<\\\\/\\\\s\\\\u\\\\p\\\\> Y\")|raw }}</strong>\n            </td>\n          </tr>\n          {% if not allDay %}\n            <tr>\n              <td> </td>\n              <td><strong>{{ dateTime|date('g:ia T') }}</strong></td>\n            </tr>\n          {% endif %}\n          {% if action != 'CANCEL' %}\n            <tr>\n              <td style=\"vertical-align: top; color: #777777;\" width=\"100px\">Attendees:</td>\n              <td>\n              {% for attendee in attendees %}\n                <a href=\"mailto:{{ attendee.email }}\" style=\"color: #2c7ea9\">{{ attendee.cn }}</a>\n                {% if attendee.role == 'CHAIR' %}(organizer){% endif %}\n                <br />\n              {% endfor %}\n            </tr>\n          {% endif %}\n          {% if location %}\n          <tr>\n            <td style=\"vertical-align: top; color: #777777;\" width=\"100px\">Where?</td>\n            <td>{{ location }}</td>\n          </tr>\n          {% endif %}\n          {% if locationImageDataAsBase64 %}\n          <tr>\n            <td colspan=\"2\">\n                <a href=\"{{ locationLink }}\" title=\"Navigate on this map\">\n                  <img src=\"data:image/png;base64,{{ locationImageDataAsBase64 }}\" width=\"100%\"/>\n                </a>\n            </td>\n          </tr>\n          {% endif %}\n          {% if url %}\n          <tr>\n            <td style=\"vertical-align: top; color: #777777;\" width=\"100px\">URL:</td>\n            <td>\n              <a href=\"{{ url }}\" style=\"color: #aaaaaa\">{{ url }}</a>\n            </td>\n          </tr>\n          {% endif %}\n          {% if description %}\n          <tr>\n            <td style=\"vertical-align: top; color: #777777;\" width=\"100px\">Notes:</td>\n            <td>{{ description }}</td>\n          </tr>\n          {% endif %}\n        </table>\n      </td>\n      <td> </td>\n    </tr>\n\n    <tr>\n      <td> </td>\n      <td style=\"border-bottom: 1px solid #cccccc\"> </td>\n      <td> </td>\n    </tr>\n  </tbody>\n\n  <tfoot>\n    <tr>\n      <td colspan=\"3\" height=\"5px\"></td>\n    </tr>\n    <tr>\n      <td> </td>\n      <td style=\"font-size: 11px; line-height: 16px; text-align: center; color: #dddddd;\">\n        Mail sent by {{ app.request.getSchemeAndHttpHost() }}\n      </td>\n      <td> </td>\n    </tr>\n  </tfoot>\n</table>\n</body>\n</html>"
  },
  {
    "path": "templates/mails/scheduling.txt.twig",
    "content": "Calendar notification from {{ senderName }}.\n\n-----------------------------------------------------------\n\n{% if action == 'REQUEST' %}\n\n**{{ senderName }}** invited you to “{{ summary }}”.\n\n{% elseif action == 'CANCEL' %}\n\n“{{ summary }}” has been canceled.\n\n{% elseif action == 'ACCEPTED' %}\n\n**{{ senderName }}** accepted your invitation to “{{ summary }}”.\n\n{% elseif action == 'TENTATIVE' %}\n\n**{{ senderName }}** tentatively accepted your invitation to “{{ summary }}”.\n\n{% elseif action == 'DECLINED' %}\n\n**{{ senderName }}** declined your invitation to “{{ summary }}”.\n\n{% endif %}\n\n-----------------------------------------------------------\n\nWhen?         {{ dateTime|date('l, F jS Y') }}\n{% if not allDay %}\n              {{ dateTime|date('g:ia T') }}\n{% endif %}\n\n{% if action != 'CANCEL' %}\nAttendees:\n{% for attendee in attendees %}\n              {{ attendee.cn }} <{{ attendee.email }}> {% if attendee.role == 'CHAIR' %}(organizer){% endif %}\n{% endfor %}\n{% endif %}\n\n\n{% if location %}\nWhere?        {{ location|replace({\"\\n\": \"\\n\" ~ '              '}) }}\n{% endif %}\n\n{% if url %}\nURL:          {{ url }}\n{% endif %}\n\n\n{% if description %}\nNotes:        {{ description|replace({\"\\n\": \"\\n\" ~ '              '}) }}\n{% endif %}\n\n-----------------------------------------------------------\n\nMail sent by {{ app.request.getSchemeAndHttpHost() }}\n"
  },
  {
    "path": "templates/security/login.html.twig",
    "content": "{% extends 'base.html.twig' %}\n{% set menu = null %}\n\n{% block body %}\n\n{% if app.user %}\n    <div class=\"mb-3\">\n        {{ \"login.already\"|trans({username: app.user.username}) }}, <a href=\"{{ path('app_logout') }}\">{{ \"logout\"|trans }}</a>\n    </div>\n{% else %}\n    <div class=\"row justify-content-md-center\">\n        <div class=\"col-md-6 col-lg-4\">\n            <form action=\"{{ path('app_login') }}\" method=\"post\">\n                {% if error %}\n                    <div class=\"alert alert-danger\">{{ error.messageKey|trans(error.messageData, 'security') }}</div>\n                {% endif %}\n\n                <h1 class=\"h3 mb-3 font-weight-normal\">{{ \"login.signin\"|trans }}</h1>\n\n                <div class=\"mb-3\">\n                    <label for=\"inputUsername\" class=\"form-label sr-only\">{{ \"login.username\"|trans }}</label>\n                    <input type=\"text\" value=\"{{ last_username }}\" name=\"_username\" id=\"inputUsername\" class=\"form-control\" placeholder=\"{{ \"login.username\"|trans }}\" required autofocus>\n                </div>\n                <div class=\"mb-3\">\n                    <label for=\"inputPassword\" class=\"form-label sr-only\">{{ \"login.password\"|trans }}</label>\n                    <input type=\"password\" name=\"_password\" id=\"inputPassword\" class=\"form-control\" placeholder=\"{{ \"login.password\"|trans }}\" required>\n                </div>\n                <button type=\"submit\" class=\"btn btn-primary\">{{ \"login.submit\"|trans }}</button>\n\n                <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ csrf_token('authenticate') }}\">\n            </form>\n        </div>\n    </div>\n{% endif %}\n\n{% endblock %}\n"
  },
  {
    "path": "templates/users/delegates.html.twig",
    "content": "{% extends 'base.html.twig' %}\n{% set menu = 'resources' %}\n\n{% block body %}\n\n{% include '_partials/back_button.html.twig' with { url: path('user_index'), text: \"users.back\"|trans } %}\n\n<h1 class=\"display-4 fw-lighter d-flex justify-content-between\">\n  {{ \"calendars.delegates.for\"|trans({'what': principal.displayName}) }}\n  {% if delegation %}\n    <a href=\"#\" data-bs-toggle=\"modal\" data-bs-target=\"#addDelegateModal\" class=\"btn btn-sm btn-success mb-auto mt-auto\">+ {{ \"calendars.delegates.add\"|trans }}</a>\n  {% endif %}\n</h1>\n\n<div class=\"list-group\">\n\n{% if delegation %}\n    <div class=\"alert alert-success d-flex justify-content-between mb-4 mt-2\" role=\"alert\">\n      <div>{{ \"delegates.enabled.text\"|trans }}<br><small>{{ \"delegates.disable.warning\"|trans }}</small></div>\n      <a href=\"{{ path('user_delegation_toggle', {userId: userId, toggle: 'off'})}}\" class=\"btn btn-sm my-auto btn-outline-danger\">{{ \"delegates.disable\"|trans }}</a>\n    </div>\n    {% for delegate in principalProxyRead.delegees %}\n        {% include '_partials/delegate_row.html.twig' with {has_write: false} %}\n    {% endfor %}\n    {% for delegate in principalProxyWrite.delegees %}\n        {% include '_partials/delegate_row.html.twig' with {has_write: true} %}\n    {% endfor %}\n\n{% else %}\n    <div class=\"alert alert-warning d-flex justify-content-between mb-4 mt-2\" role=\"alert\">\n      {{ \"delegates.disabled.text\"|trans }}<a href=\"{{ path('user_delegation_toggle', {userId: userId, toggle: 'on'})}}\" class=\"btn btn-sm my-auto btn-warning\">{{ \"delegates.enable\"|trans }}</a>\n    </div>\n{% endif %}\n\n</div>\n\n{% include '_partials/delete_modal.html.twig' with {flavour: 'delegates'} %}\n{% include '_partials/add_delegate_modal.html.twig' with {principals: allPrincipals} %}\n\n{% endblock %}"
  },
  {
    "path": "templates/users/edit.html.twig",
    "content": "{% extends 'base.html.twig' %}\n{% set menu = 'resources' %}\n\n{% block body %}\n\n{% include '_partials/back_button.html.twig' with { url: path('user_index'), text: \"users.back\"|trans } %}\n\n{% if username %}\n<h1 class=\"display-4 fw-lighter mb-5\">{{ \"users.edit\"|trans({'username': username }) }}</h1>\n{% else %}\n<h1 class=\"display-4 fw-lighter mb-5\">{{ \"users.new\"|trans }}</h1>\n{% endif %}\n\n{{ form(form) }}\n\n{% endblock %}"
  },
  {
    "path": "templates/users/index.html.twig",
    "content": "{% extends 'base.html.twig' %}\n{% set menu = 'resources' %}\n\n{% block body %}\n\n<h1 class=\"display-4 d-flex fw-lighter justify-content-between\">{{ \"title.users_and_resources\"|trans }}<a href=\"{{ path('user_create') }}\" class=\"btn btn-sm btn-success mb-auto mt-auto\">+ {{ \"users.new\"|trans }}</a></h1>\n\n<div class=\"list-group mt-4\">\n{% for result in results %}\n    {% set principal = result[0] %}\n    {% set userId = result.userId %}\n    <div class=\"list-group-item p-3\">\n        <div class=\"d-flex w-100 justify-content-between\">\n            <h5 class=\"me-auto\">{{ principal.displayName }} <a href=\"mailto:{{ principal.email }}\">&lsaquo;{{ principal.email }}&rsaquo;</a></h5>\n\n            <div class=\"me-0 text-right d-lg-block d-none\">\n                <a href=\"{{ path('user_delegates',{userId: userId}) }}\" class=\"btn btn-sm btn-outline-secondary mb-1\">💁&nbsp;{{ \"users.delegates\"|trans }}</a>\n                <a href=\"{{ path('calendar_index',{userId: userId}) }}\" class=\"btn btn-sm btn-outline-secondary mb-1\">🗓&nbsp;{{ \"users.calendars\"|trans }}</a>\n                <a href=\"{{ path('addressbook_index',{userId: userId})}}\" class=\"btn btn-sm btn-outline-secondary ms-1 mb-1\">📖&nbsp;{{ \"users.addressbooks\"|trans }}</a>\n                <a href=\"{{ path('user_edit',{userId: userId})}}\" class=\"btn btn-sm btn-outline-primary ms-1 mb-1\">✎&nbsp;{{ \"edit\"|trans }}</a>\n                <a href=\"#\" \n                    data-bs-toggle=\"modal\" data-bs-target=\"#deleteModal-users\"\n                    data-href=\"{{ path('user_delete',{userId: userId})}}\" \n                    class=\"btn btn-sm btn-outline-danger ms-1 mb-1\" \n                    data-flavour=\"users\"\n                >⚠&nbsp;{{ \"delete\"|trans }}</a>\n            </div>\n        </div>\n        <p class=\"mb-1\">{{ \"users.username\"|trans }} : <code>{{ principal.username }}</code></p>\n        <small>{{ \"users.uri\"|trans }} : <code>{{ principal.uri }}</code>{% if principal.isAdmin %} — <span class=\"badge bg-primary\">{{ \"users.administrator\"|trans }}</span>{% endif %}</small>\n        <div class=\"btn-group btn-group-sm mt-3 d-flex d-lg-none\" role=\"group\">\n            <a href=\"{{ path('user_delegates',{userId: userId}) }}\" class=\"btn btn-outline-secondary flex-fill w-100\"><span class=\"d-none d-sm-inline\">💁&nbsp;</span>{{ \"users.delegates\"|trans }}</a>\n            <a href=\"{{ path('calendar_index',{userId: userId}) }}\" class=\"btn btn-outline-secondary flex-fill w-100\"><span class=\"d-none d-sm-inline\">🗓&nbsp;</span>{{ \"users.calendars\"|trans }}</a>\n            <a href=\"{{ path('addressbook_index',{userId: userId})}}\" class=\"btn btn-outline-secondary flex-fill w-100\"><span class=\"d-none d-sm-inline\">📖&nbsp;</span>{{ \"users.addressbooks\"|trans }}</a>\n            <a href=\"{{ path('user_edit',{userId: userId})}}\" class=\"btn btn-outline-primary flex-fill flex-shrink-1\"><span class=\"d-none d-sm-inline\">✎&nbsp;</span>{{ \"edit\"|trans }}</a>\n            <a href=\"#\" \n                data-bs-toggle=\"modal\" data-bs-target=\"#deleteModal-users\"\n                data-href=\"{{ path('user_delete',{userId: userId})}}\" \n                class=\"btn btn-outline-danger flex-fill flex-shrink-1\" \n                data-flavour=\"users\"\n            ><span class=\"d-none d-sm-inline\">⚠&nbsp;</span>{{ \"delete\"|trans }}</a>\n        </div>\n    </div>\n{% else %}\n    <div id=\"no-user\" class=\"my-5 py-5 d-flex justify-content-center align-items-center text-muted\">{{ \"no.users.yet\"|trans }}</div>\n{% endfor %}\n</div>\n\n{% include '_partials/delete_modal.html.twig' with {flavour: 'users'} %}\n\n{% endblock %}"
  },
  {
    "path": "tests/.gitignore",
    "content": ""
  },
  {
    "path": "tests/Functional/Commands/SyncBirthdayCalendarTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Tests\\Functional\\Command;\n\nuse App\\Constants;\nuse App\\Entity\\AddressBook;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\CalendarObject;\nuse App\\Entity\\Card;\nuse App\\Entity\\Principal;\nuse App\\Entity\\User;\nuse App\\Services\\BirthdayService;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Sabre\\CalDAV\\Backend\\PDO as CalendarBackend;\nuse Symfony\\Bundle\\FrameworkBundle\\Console\\Application;\nuse Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase;\nuse Symfony\\Component\\Console\\Tester\\CommandTester;\n\nclass SyncBirthdayCalendarTest extends KernelTestCase\n{\n    private EntityManagerInterface $em;\n    private CommandTester $commandTester;\n\n    protected function setUp(): void\n    {\n        self::bootKernel();\n\n        $this->em = static::getContainer()->get(EntityManagerInterface::class);\n\n        $birthdayService = static::getContainer()->get(BirthdayService::class);\n        $pdo = $this->em->getConnection()->getNativeConnection();\n        $birthdayService->setBackend(new CalendarBackend($pdo));\n\n        $application = new Application(self::$kernel);\n        $command = $application->find('dav:sync-birthday-calendar');\n        $this->commandTester = new CommandTester($command);\n\n        $this->em->getConnection()->beginTransaction();\n    }\n\n    protected function tearDown(): void\n    {\n        $this->em->getConnection()->rollBack();\n        parent::tearDown();\n    }\n\n    private function createUser(string $username): User\n    {\n        $user = (new User())\n            ->setUsername($username)\n            ->setPassword('hashed');\n        $this->em->persist($user);\n\n        $principal = (new Principal())\n            ->setUri(Principal::PREFIX.$username)\n            ->setEmail($username.'@example.com')\n            ->setDisplayName($username);\n        $this->em->persist($principal);\n\n        $this->em->flush();\n\n        return $user;\n    }\n\n    private function createAddressBookWithCard(string $username, string $cardUri, string $cardData): AddressBook\n    {\n        $addressBook = (new AddressBook())\n            ->setPrincipalUri(Principal::PREFIX.$username)\n            ->setUri('default')\n            ->setDisplayName('Default')\n            ->setDescription('')\n            ->setSynctoken('1')\n            ->setIncludedInBirthdayCalendar(true);\n        $this->em->persist($addressBook);\n\n        $card = (new Card())\n            ->setAddressBook($addressBook)\n            ->setUri($cardUri)\n            ->setCarddata($cardData)\n            ->setLastmodified(time())\n            ->setSize(strlen($cardData))\n            ->setEtag(md5($cardData));\n        $this->em->persist($card);\n\n        $this->em->flush();\n\n        return $addressBook;\n    }\n\n    private function assertBirthdayEventExists(string $principalUri, string $addressBookUri, string $cardUri, string $expectedNameFragment): void\n    {\n        $instance = $this->em->getRepository(CalendarInstance::class)->findOneBy([\n            'principalUri' => $principalUri,\n            'uri' => Constants::BIRTHDAY_CALENDAR_URI,\n        ]);\n        $this->assertNotNull($instance, \"Birthday calendar instance not found for $principalUri\");\n\n        $objectUri = $addressBookUri.'-'.$cardUri.'.ics';\n        $object = $this->em->getRepository(CalendarObject::class)->findOneBy([\n            'calendar' => $instance->getCalendar(),\n            'uri' => $objectUri,\n        ]);\n        $this->assertNotNull($object, \"Calendar object $objectUri not found\");\n        $this->assertStringContainsString($expectedNameFragment, $object->getCalendarData());\n    }\n\n    public function testExecuteSyncsAllUsers(): void\n    {\n        $this->createUser('alice');\n        $this->createUser('bob');\n        $this->createAddressBookWithCard(\n            'alice',\n            'alice-contact.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:Alice Contact\\r\\nUID:alice-1\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\"\n        );\n        $this->createAddressBookWithCard(\n            'bob',\n            'bob-contact.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:Bob Contact\\r\\nUID:bob-1\\r\\nBDAY:19850320\\r\\nEND:VCARD\\r\\n\"\n        );\n\n        $this->commandTester->execute([]);\n\n        $this->assertSame(0, $this->commandTester->getStatusCode());\n        $this->assertStringContainsString('Start birthday calendar sync for all users', $this->commandTester->getDisplay());\n\n        $this->em->clear();\n\n        $this->assertBirthdayEventExists(Principal::PREFIX.'alice', 'default', 'alice-contact.vcf', 'Alice Contact');\n        $this->assertBirthdayEventExists(Principal::PREFIX.'bob', 'default', 'bob-contact.vcf', 'Bob Contact');\n    }\n\n    public function testExecuteSyncsSingleUser(): void\n    {\n        $this->createUser('alice');\n        $this->createUser('bob');\n        $this->createAddressBookWithCard(\n            'alice',\n            'alice-contact.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:Alice Contact\\r\\nUID:alice-1\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\"\n        );\n        $this->createAddressBookWithCard(\n            'bob',\n            'bob-contact.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:Bob Contact\\r\\nUID:bob-1\\r\\nBDAY:19850320\\r\\nEND:VCARD\\r\\n\"\n        );\n\n        $this->commandTester->execute(['username' => 'alice']);\n\n        $this->assertSame(0, $this->commandTester->getStatusCode());\n        $this->assertStringContainsString('Start birthday calendar sync for alice', $this->commandTester->getDisplay());\n\n        $this->em->clear();\n\n        // Alice's birthday calendar should exist with the event\n        $this->assertBirthdayEventExists(Principal::PREFIX.'alice', 'default', 'alice-contact.vcf', 'Alice Contact');\n\n        // Bob's birthday calendar should NOT have been created\n        $bobInstance = $this->em->getRepository(CalendarInstance::class)->findOneBy([\n            'principalUri' => Principal::PREFIX.'bob',\n            'uri' => Constants::BIRTHDAY_CALENDAR_URI,\n        ]);\n        $this->assertNull($bobInstance);\n    }\n\n    public function testExecuteThrowsExceptionForUnknownUser(): void\n    {\n        $this->expectException(\\InvalidArgumentException::class);\n        $this->expectExceptionMessage('User <unknown> is unknown.');\n\n        $this->commandTester->execute(['username' => 'unknown']);\n    }\n\n    public function testExecuteWithNoUsersInDatabaseSucceeds(): void\n    {\n        $this->commandTester->execute([]);\n\n        $this->assertSame(0, $this->commandTester->getStatusCode());\n\n        $instances = $this->em->getRepository(CalendarInstance::class)->findBy(['uri' => Constants::BIRTHDAY_CALENDAR_URI]);\n        $this->assertCount(0, $instances);\n    }\n}\n"
  },
  {
    "path": "tests/Functional/Controllers/AddressBookControllerTest.php",
    "content": "<?php\n\nnamespace App\\Tests\\Functional;\n\nuse App\\Entity\\AddressBook;\nuse App\\Entity\\User;\nuse App\\Security\\AdminUser;\nuse Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase;\n\nclass AddressBookControllerTest extends WebTestCase\n{\n    private function getUserId($client, string $username): int\n    {\n        $userRepository = static::getContainer()->get('doctrine.orm.entity_manager')->getRepository(User::class);\n        $user = $userRepository->findOneByUsername($username);\n\n        return $user->getId();\n    }\n\n    public function testAddressBookIndex(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $client->request('GET', '/addressbooks/'.$userId);\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorExists('nav.navbar');\n        $this->assertSelectorTextContains('h1', 'Address books for Test User');\n        $this->assertSelectorTextContains('a.btn', '+ New Address Book');\n        $this->assertSelectorTextContains('h5', 'default.addressbook.title');\n    }\n\n    public function testAddressBookEdit(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $addressbookRepository = static::getContainer()->get('doctrine.orm.entity_manager')->getRepository(AddressBook::class);\n        $addressbook = $addressbookRepository->findOneByDisplayName('default.addressbook.title');\n\n        $client->request('GET', '/addressbooks/'.$userId.'/edit/'.$addressbook->getId());\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorTextContains('h1', 'Editing Address Book «default.addressbook.title»');\n        $this->assertSelectorTextContains('button#address_book_save', 'Save');\n\n        $client->submitForm('address_book_save');\n\n        $this->assertResponseRedirects('/addressbooks/'.$userId);\n        $client->followRedirect();\n\n        $this->assertSelectorTextContains('h5', 'default.addressbook.title');\n    }\n\n    public function testAddressBookNew(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $crawler = $client->request('GET', '/addressbooks/'.$userId.'/new');\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorTextContains('h1', 'New Address Book ');\n        $this->assertSelectorTextContains('button#address_book_save', 'Save');\n\n        $buttonCrawlerNode = $crawler->selectButton('address_book_save');\n\n        $form = $buttonCrawlerNode->form();\n        $client->submit($form, [\n            'address_book[uri]' => 'new_test_address_book',\n            'address_book[displayName]' => 'New test address book',\n            'address_book[description]' => 'new address book',\n        ]);\n\n        $this->assertResponseRedirects('/addressbooks/'.$userId);\n        $client->followRedirect();\n\n        $this->assertSelectorTextContains('h5', 'default.addressbook.title');\n        $this->assertAnySelectorTextContains('h5', 'New test address book');\n    }\n\n    public function testAddressBookDelete(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $addressbookRepository = static::getContainer()->get('doctrine.orm.entity_manager')->getRepository(AddressBook::class);\n        $addressbook = $addressbookRepository->findOneByDisplayName('default.addressbook.title');\n\n        $client->request('GET', '/addressbooks/'.$userId.'/delete/'.$addressbook->getId());\n\n        $this->assertResponseRedirects('/addressbooks/'.$userId);\n        $client->followRedirect();\n\n        $this->assertSelectorTextNotContains('h5', 'default.addressbook.title');\n    }\n}\n"
  },
  {
    "path": "tests/Functional/Controllers/ApiControllerTest.php",
    "content": "<?php\n\nnamespace App\\Tests\\Functional;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase;\n\nclass ApiControllerTest extends WebTestCase\n{\n    /*\n     * Helper function to get an existing user ID from the user list\n     *\n     * @param int  $index   Index of the user in the list (0 - first user, 1 - second user)\n     * @param mixed $client\n     *\n     * @return int User ID\n     */\n    private function getUserId($client, int $index): int\n    {\n        $client->request('GET', '/api/v1/users', [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n\n        $this->assertArrayHasKey('data', $data);\n        $this->assertStringContainsString('test_user', $data['data'][$index]['username']);\n\n        return $data['data'][$index]['user_id'];\n    }\n\n    /*\n     * Helper function to get an existing username from the user list\n     *\n     * @param int  $index   Index of the user in the list (0 - first user, 1 - second user)\n     * @param mixed $client\n     *\n     * @return string Username\n     */\n    private function getUserUsername($client, int $index): string\n    {\n        $client->request('GET', '/api/v1/users', [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n\n        $this->assertArrayHasKey('data', $data);\n        $this->assertStringContainsString('test_user', $data['data'][$index]['username']);\n\n        return $data['data'][$index]['username'];\n    }\n\n    /*\n     * Helper function to get an existing calendar ID from the user calendar list\n     *\n     * @param mixed $client\n     * @param int   $userId\n     * @param bool  $default  Whether to get the default calendar (true) or the second calendar (false)\n     *\n     * @return int Calendar ID\n     */\n    private function getCalendarId($client, int $userId, bool $default = true): int\n    {\n        $client->request('GET', '/api/v1/calendars/'.$userId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n\n        if ($default) {\n            $this->assertMatchesRegularExpression('/^\\d+$/', $data['data']['user_calendars'][0]['id']);\n\n            return $data['data']['user_calendars'][0]['id'];\n        }\n        $this->assertMatchesRegularExpression('/^\\d+$/', $data['data']['user_calendars'][1]['id']);\n\n        return $data['data']['user_calendars'][1]['id'];\n    }\n\n    /*\n     * Test the health endpoint\n     */\n    public function testHealth(): void\n    {\n        $client = static::createClient();\n        $client->request('GET', '/api/v1/health');\n\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('OK', $data['status']);\n    }\n\n    /*\n     * Test the API endpoint with invalid token\n     */\n    public function testApiInvalidToken(): void\n    {\n        $client = static::createClient();\n        $client->request('GET', '/api/v1/users', [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => 'invalid_token',\n        ]);\n        $this->assertResponseStatusCodeSame(401);\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('error', $data['status']);\n        $this->assertEquals('Invalid X-Davis-API-Token header', $data['message']);\n    }\n\n    /*\n     * Test the API endpoint with missing token\n     */\n    public function testApiMissingToken(): void\n    {\n        $client = static::createClient();\n        $client->request('GET', '/api/v1/users', [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n        ]);\n        $this->assertResponseStatusCodeSame(401);\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('error', $data['status']);\n        $this->assertEquals('Missing X-Davis-API-Token header', $data['message']);\n    }\n\n    /*\n     * Test the user list endpoint\n     */\n    public function testUserList(): void\n    {\n        $client = static::createClient();\n        $client->request('GET', '/api/v1/users', [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n\n        // Check if user1 is present in db\n        $this->assertArrayHasKey('user_id', $data['data'][0]);\n        $this->assertArrayHasKey('principal_id', $data['data'][0]);\n        $this->assertArrayHasKey('uri', $data['data'][0]);\n        $this->assertStringContainsString('principals/test_user', $data['data'][0]['uri']);\n        $this->assertArrayHasKey('username', $data['data'][0]);\n        $this->assertStringContainsString('test_user', $data['data'][0]['username']);\n\n        // Check if user2 is present in db\n        $this->assertArrayHasKey('user_id', $data['data'][1]);\n        $this->assertArrayHasKey('principal_id', $data['data'][1]);\n        $this->assertArrayHasKey('uri', $data['data'][1]);\n        $this->assertStringContainsString('principals/test_user2', $data['data'][1]['uri']);\n        $this->assertArrayHasKey('username', $data['data'][1]);\n        $this->assertStringContainsString('test_user2', $data['data'][1]['username']);\n    }\n\n    /*\n     * Test the user details endpoint\n     */\n    public function testUserDetails(): void\n    {\n        // Create client once\n        $client = static::createClient();\n\n        // Get userId and username from existing user lists\n        $userId = $this->getUserId($client, 0);\n        $username = $this->getUserUsername($client, 0);\n\n        // Check user details endpoint\n        $client->request('GET', '/api/v1/users/'.$userId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n\n        // Check if user details are correct\n        $this->assertArrayHasKey('user_id', $data['data']);\n        $this->assertEquals($userId, $data['data']['user_id']);\n        $this->assertArrayHasKey('displayname', $data['data']);\n        $this->assertStringContainsString('Test User', $data['data']['displayname']);\n        $this->assertArrayHasKey('email', $data['data']);\n        $this->assertStringContainsString('test@test.com', $data['data']['email']);\n        $this->assertStringEqualsStringIgnoringLineEndings($username, $data['data']['username']);\n    }\n\n    /*\n     * Test the user calendars list endpoint\n     */\n    public function testUserCalendarsList(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n\n        $client->request('GET', '/api/v1/calendars/'.$userId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n\n        // Check if calendar list is correct\n        $this->assertArrayHasKey('status', $data);\n        $this->assertEquals('success', $data['status']);\n        $this->assertArrayHasKey('user_calendars', $data['data']);\n        $this->assertStringContainsString('default', $data['data']['user_calendars'][0]['uri']);\n        $this->assertStringContainsString('default.calendar.title', $data['data']['user_calendars'][0]['displayname']);\n        $this->assertArrayHasKey('shared_calendars', $data['data']);\n        $this->assertArrayHasKey('subscriptions', $data['data']);\n    }\n\n    /*\n     * Test the user calendar details endpoint\n     */\n    public function testUserCalendarDetails(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n\n        // Get calendar list to retrieve calendar ID\n        $client->request('GET', '/api/v1/calendars/'.$userId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $calendar_id = $data['data']['user_calendars'][0]['id'];\n\n        // Check calendar details endpoint\n        $client->request('GET', '/api/v1/calendars/'.$userId.'/'.$calendar_id, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n\n        // Check if calendar details are correct\n        $this->assertArrayHasKey('id', $data['data']);\n        $this->assertArrayHasKey('uri', $data['data']);\n        $this->assertStringContainsString('default', $data['data']['uri']);\n        $this->assertArrayHasKey('displayname', $data['data']);\n        $this->assertStringContainsString('default.calendar.title', $data['data']['displayname']);\n\n        $this->assertArrayHasKey('description', $data['data']);\n        $this->assertStringContainsString('default.calendar.description', $data['data']['description']);\n\n        $this->assertArrayHasKey('events', $data['data']);\n        $this->assertArrayHasKey('notes', $data['data']);\n        $this->assertArrayHasKey('tasks', $data['data']);\n    }\n\n    /*\n     * Test creating a new user calendar\n     */\n    public function testCreateUserCalendar(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n\n        // Create calendar API request with JSON body\n        $payload = [\n            'uri' => 'api_calendar',\n            'name' => 'api.calendar.title',\n            'description' => 'api.calendar.description',\n            'events_support' => true,\n            'tasks_support' => true,\n            'notes_support' => false,\n        ];\n\n        $client->request('POST', '/api/v1/calendars/'.$userId.'/create', [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n            'CONTENT_TYPE' => 'application/json',\n        ], json_encode($payload));\n\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n\n        $this->assertArrayHasKey('status', $data);\n        $this->assertEquals('success', $data['status']);\n        $this->assertArrayHasKey('data', $data);\n        $this->assertMatchesRegularExpression('/^\\d+$/', $data['data']['calendar_id']);\n        $this->assertStringContainsString('api_calendar', $data['data']['calendar_uri']);\n\n        // Check if calendar is created\n        $calendarId = $data['data']['calendar_id'];\n        $client->request('GET', '/api/v1/calendars/'.$userId.'/'.$calendarId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n\n        $this->assertArrayHasKey('events', $data['data']);\n        $this->assertTrue($data['data']['events']['enabled']);\n        $this->assertArrayHasKey('tasks', $data['data']);\n        $this->assertTrue($data['data']['tasks']['enabled']);\n        $this->assertArrayHasKey('notes', $data['data']);\n        $this->assertFalse($data['data']['notes']['enabled']);\n    }\n\n    /*\n     * Test editing a user calendar\n     */\n    public function testEditUserCalendar(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n        $calendarId = $this->getCalendarId($client, $userId, true);\n\n        // Edit user default calendar\n        $payload = [\n            'name' => 'api.calendar.edited.title',\n            'description' => 'api.calendar.edited.description',\n            'events_support' => true,\n            'tasks_support' => true,\n            'notes_support' => true,\n        ];\n        $client->request('PATCH', '/api/v1/calendars/'.$userId.'/'.$calendarId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n            'CONTENT_TYPE' => 'application/json',\n        ], json_encode($payload));\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertArrayHasKey('status', $data);\n        $this->assertEquals('success', $data['status']);\n\n        // Check if edits were applied\n        $client->request('GET', '/api/v1/calendars/'.$userId.'/'.$calendarId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertArrayHasKey('data', $data);\n        $this->assertStringContainsString($payload['name'], $data['data']['displayname']);\n        $this->assertStringContainsString($payload['description'], $data['data']['description']);\n\n        $this->assertArrayHasKey('events', $data['data']);\n        $this->assertTrue($data['data']['events']['enabled']);\n        $this->assertArrayHasKey('tasks', $data['data']);\n        $this->assertTrue($data['data']['tasks']['enabled']);\n        $this->assertArrayHasKey('notes', $data['data']);\n        $this->assertTrue($data['data']['notes']['enabled']);\n    }\n\n    /*\n     * Test getting shares for a user calendar (should be empty initially)\n     */\n    public function testGetUserCalendarSharesEmpty(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n        $calendarId = $this->getCalendarId($client, $userId, true);\n\n        // Get shares for user default calendar\n        $client->request('GET', '/api/v1/calendars/'.$userId.'/shares/'.$calendarId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertArrayHasKey('status', $data);\n        $this->assertEquals('success', $data['status']);\n        $this->assertArrayHasKey('data', $data);\n        $this->assertEmpty($data['data']);\n    }\n\n    /*\n     * Test sharing user calendar to another user\n     */\n    public function testShareUserCalendar(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n        $shareeUsername = $this->getUserUsername($client, 1);\n        $calendarId = $this->getCalendarId($client, $userId, true);\n\n        // Share user default calendar to test_user2\n        $payload = [\n            'username' => $shareeUsername,\n            'write_access' => false,\n        ];\n        $client->request('POST', '/api/v1/calendars/'.$userId.'/share/'.$calendarId.'/add', [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n            'CONTENT_TYPE' => 'application/json',\n        ], json_encode($payload));\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertArrayHasKey('status', $data);\n        $this->assertEquals('success', $data['status']);\n\n        // Check if share was applied\n        $client->request('GET', '/api/v1/calendars/'.$userId.'/shares/'.$calendarId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('success', $data['status']);\n        $this->assertArrayHasKey('data', $data);\n        $this->assertStringContainsString($shareeUsername, $data['data'][0]['username']);\n        $this->assertArrayHasKey('user_id', $data['data'][0]);\n        $this->assertIsNumeric($data['data'][0]['user_id']);\n        $this->assertFalse($data['data'][0]['write_access']);\n    }\n\n    /*\n     * Test removing shared access to user calendar\n     */\n    public function testUnshareUserCalendar(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n        $shareeUsername = $this->getUserUsername($client, 1);\n        $calendarId = $this->getCalendarId($client, $userId, true);\n\n        // Unshare user default calendar from test_user2\n        $payload = [\n            'username' => $shareeUsername,\n        ];\n        $client->request('POST', '/api/v1/calendars/'.$userId.'/share/'.$calendarId.'/remove', [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n            'CONTENT_TYPE' => 'application/json',\n        ], json_encode($payload));\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('success', $data['status']);\n\n        // Check if unshare was applied\n        $client->request('GET', '/api/v1/calendars/'.$userId.'/shares/'.$calendarId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('success', $data['status']);\n        $this->assertArrayHasKey('data', $data);\n        $this->assertEmpty($data['data']);\n    }\n\n    /*\n     * Test creating a calendar with no components enabled should return validation error\n     */\n    public function testCreateUserCalendarNoComponents(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n\n        // Create calendar API request with no components enabled\n        $payload = [\n            'uri' => 'no_components_calendar',\n            'name' => 'no.components.calendar',\n            'description' => 'no.components.description',\n            'events_support' => false,\n            'tasks_support' => false,\n            'notes_support' => false,\n        ];\n\n        $client->request('POST', '/api/v1/calendars/'.$userId.'/create', [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n            'CONTENT_TYPE' => 'application/json',\n        ], json_encode($payload));\n\n        $this->assertResponseStatusCodeSame(400);\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('error', $data['status']);\n        $this->assertStringContainsString('At least one calendar component must be enabled', $data['message']);\n    }\n\n    /*\n     * Test editing a calendar with no components enabled should return validation error\n     */\n    public function testEditUserCalendarNoComponents(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n        $calendarId = $this->getCalendarId($client, $userId, true);\n\n        // Edit calendar API request with no components enabled\n        $payload = [\n            'name' => 'edited.calendar.title',\n            'description' => 'edited.calendar.description',\n            'events_support' => false,\n            'tasks_support' => false,\n            'notes_support' => false,\n        ];\n\n        $client->request('PUT', '/api/v1/calendars/'.$userId.'/'.$calendarId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n            'CONTENT_TYPE' => 'application/json',\n        ], json_encode($payload));\n\n        $this->assertResponseStatusCodeSame(400);\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('error', $data['status']);\n        $this->assertStringContainsString('At least one calendar component must be enabled', $data['message']);\n    }\n\n    /*\n    * Test deleting a user calendar\n    */\n    public function testDeleteUserCalendar(): void\n    {\n        $client = static::createClient();\n        $userId = $this->getUserId($client, 0);\n        $calendarId = $this->getCalendarId($client, $userId, true);\n\n        // Delete the calendar\n        $client->request('DELETE', '/api/v1/calendars/'.$userId.'/'.$calendarId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n            'CONTENT_TYPE' => 'application/json',\n        ]);\n\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('success', $data['status']);\n\n        // Check if calendar is deleted\n        $client->request('GET', '/api/v1/calendars/'.$userId.'/'.$calendarId, [], [], [\n            'HTTP_ACCEPT' => 'application/json',\n            'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],\n        ]);\n        $this->assertResponseIsSuccessful();\n        $this->assertResponseHeaderSame('Content-Type', 'application/json');\n\n        $data = json_decode($client->getResponse()->getContent(), true);\n        $this->assertEquals('success', $data['status']);\n        $this->assertEmpty($data['data']);\n    }\n}\n"
  },
  {
    "path": "tests/Functional/Controllers/CalendarControllerTest.php",
    "content": "<?php\n\nnamespace App\\Tests\\Functional;\n\nuse App\\Entity\\User;\nuse App\\Repository\\CalendarInstanceRepository;\nuse App\\Security\\AdminUser;\nuse Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase;\n\nclass CalendarControllerTest extends WebTestCase\n{\n    private function getUserId($client, string $username): int\n    {\n        $userRepository = static::getContainer()->get('doctrine.orm.entity_manager')->getRepository(User::class);\n        $user = $userRepository->findOneByUsername($username);\n\n        return $user->getId();\n    }\n\n    public function testCalendarIndex(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $client->request('GET', '/calendars/'.$userId);\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorExists('nav.navbar');\n        $this->assertSelectorTextContains('h1', 'Calendars for Test User');\n        $this->assertSelectorTextContains('a.btn', '+ New Calendar');\n        $this->assertSelectorTextContains('h5', 'default.calendar.title');\n    }\n\n    public function testCalendarEdit(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $calendarRepository = static::getContainer()->get(CalendarInstanceRepository::class);\n        $calendar = $calendarRepository->findOneByDisplayName('default.calendar.title');\n\n        $client->request('GET', '/calendars/'.$userId.'/edit/'.$calendar->getId());\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorTextContains('h1', 'Editing Calendar «default.calendar.title»');\n        $this->assertSelectorTextContains('button#calendar_instance_save', 'Save');\n\n        $client->submitForm('calendar_instance_save');\n\n        $this->assertResponseRedirects('/calendars/'.$userId);\n        $client->followRedirect();\n\n        $this->assertSelectorTextContains('h5', 'default.calendar.title');\n    }\n\n    public function testCalendarNew(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $crawler = $client->request('GET', '/calendars/'.$userId.'/new');\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorTextContains('h1', 'New Calendar ');\n        $this->assertSelectorTextContains('button#calendar_instance_save', 'Save');\n\n        $buttonCrawlerNode = $crawler->selectButton('calendar_instance_save');\n\n        $form = $buttonCrawlerNode->form();\n        $client->submit($form, [\n            'calendar_instance[uri]' => 'new_test_calendar',\n            'calendar_instance[displayName]' => 'New test calendar',\n            'calendar_instance[description]' => 'new calendar',\n            'calendar_instance[calendarColor]' => '#00112233',\n        ]);\n\n        $this->assertResponseRedirects('/calendars/'.$userId);\n        $client->followRedirect();\n\n        $this->assertSelectorTextContains('h5', 'default.calendar.title');\n        $this->assertAnySelectorTextContains('h5', 'New test calendar');\n    }\n\n    public function testCalendarDelete(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $calendarRepository = static::getContainer()->get(CalendarInstanceRepository::class);\n        $calendar = $calendarRepository->findOneByDisplayName('default.calendar.title');\n\n        $client->request('GET', '/calendars/'.$userId.'/delete/'.$calendar->getId());\n\n        $this->assertResponseRedirects('/calendars/'.$userId);\n        $client->followRedirect();\n\n        $this->assertSelectorTextNotContains('h5', 'default.calendar.title');\n    }\n}\n"
  },
  {
    "path": "tests/Functional/Controllers/DashboardTest.php",
    "content": "<?php\n\nnamespace App\\Tests\\Functional;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase;\n\nclass DashboardTest extends WebTestCase\n{\n    public function testIndexPage(): void\n    {\n        $client = static::createClient();\n        $client->request('GET', '/');\n\n        $this->assertResponseIsSuccessful();\n        $this->assertSelectorTextContains('h3', 'Davis');\n\n        $this->assertSelectorExists('li.caldav');\n        $this->assertSelectorExists('li.carddav');\n        $this->assertSelectorExists('li.webdav');\n    }\n\n    public function testDashboardPageUnlogged(): void\n    {\n        $client = static::createClient();\n        $client->request('GET', '/dashboard');\n\n        $this->assertResponseRedirects('/login');\n    }\n\n    public function testLoginPage(): void\n    {\n        $client = static::createClient();\n        $client->request('GET', '/login');\n\n        $this->assertResponseIsSuccessful();\n        $this->assertSelectorTextContains('h1', 'Please sign in');\n        $this->assertSelectorExists('nav.navbar');\n    }\n\n    public function testLoginIncorrectUsername(): void\n    {\n        $client = static::createClient();\n        $crawler = $client->request('GET', '/login');\n\n        $form = $crawler->selectButton('Submit')->form();\n        $form['_username']->setValue('bad_'.$_ENV['ADMIN_LOGIN']);\n        $form['_password']->setValue('bad_password');\n\n        $client->submit($form);\n        $this->assertResponseRedirects('/login');\n        $crawler = $client->followRedirect();\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorTextContains('div.alert.alert-danger', 'Invalid credentials.');\n    }\n\n    public function testLoginIncorrectPassword(): void\n    {\n        $client = static::createClient();\n        $crawler = $client->request('GET', '/login');\n\n        $form = $crawler->selectButton('Submit')->form();\n        $form['_username']->setValue($_ENV['ADMIN_LOGIN']);\n        $form['_password']->setValue('bad_password');\n\n        $client->submit($form);\n        $this->assertResponseRedirects('/login');\n        $crawler = $client->followRedirect();\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorTextContains('div.alert.alert-danger', 'Invalid credentials.');\n    }\n\n    public function testLoginCorrect(): void\n    {\n        $client = static::createClient();\n        $crawler = $client->request('GET', '/login');\n\n        $form = $crawler->selectButton('Submit')->form();\n        $form['_username']->setValue($_ENV['ADMIN_LOGIN']);\n        $form['_password']->setValue($_ENV['ADMIN_PASSWORD']);\n\n        $client->submit($form);\n        $this->assertResponseRedirects('/dashboard');\n        $crawler = $client->followRedirect();\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorTextContains('h1', 'Dashboard');\n        $this->assertSelectorTextContains('h3.capabilities', 'Capabilities');\n        $this->assertSelectorTextContains('h3.objects', 'Objects');\n        $this->assertSelectorTextContains('h3.environment', 'Configured environment');\n        $this->assertSelectorExists('nav.navbar');\n    }\n}\n"
  },
  {
    "path": "tests/Functional/Controllers/UserControllerTest.php",
    "content": "<?php\n\nnamespace App\\Tests\\Functional;\n\nuse App\\Entity\\User;\nuse App\\Security\\AdminUser;\nuse Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase;\n\nclass UserControllerTest extends WebTestCase\n{\n    private function getUserId($client, string $username): int\n    {\n        $userRepository = static::getContainer()->get('doctrine.orm.entity_manager')->getRepository(User::class);\n        $user = $userRepository->findOneByUsername($username);\n\n        return $user->getId();\n    }\n\n    public function testUserIndex(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n        $client->request('GET', '/users/');\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorExists('nav.navbar');\n        $this->assertSelectorTextContains('h1', 'Users and Resources');\n        $this->assertSelectorTextContains('a.btn', '+ New User');\n        $this->assertAnySelectorTextContains('h5', 'Test User');\n    }\n\n    public function testUserEdit(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $client->request('GET', '/users/edit/'.$userId);\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorTextContains('h1', 'Editing User «test_user»');\n        $this->assertSelectorTextContains('button#user_save', 'Save');\n\n        $client->submitForm('user_save');\n\n        $this->assertResponseRedirects('/users/');\n        $client->followRedirect();\n\n        $this->assertAnySelectorTextContains('h5', 'Test User');\n    }\n\n    public function testUserNew(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n        $crawler = $client->request('GET', '/users/new');\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorTextContains('h1', 'New User');\n        $this->assertSelectorTextContains('button#user_save', 'Save');\n\n        $buttonCrawlerNode = $crawler->selectButton('user_save');\n\n        $form = $buttonCrawlerNode->form();\n        $client->submit($form, [\n            'user[username]' => 'new_test_user',\n            'user[displayName]' => 'New test User',\n            'user[email]' => 'coucou@coucou.com',\n            'user[password][first]' => 'coucou',\n            'user[password][second]' => 'coucou',\n            'user[isAdmin]' => false,\n        ]);\n\n        $this->assertResponseRedirects('/users/');\n        $client->followRedirect();\n\n        $this->assertAnySelectorTextContains('h5', 'Test User');\n        $this->assertAnySelectorTextContains('h5', 'New test User');\n    }\n\n    public function testUserDelete(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId1 = $this->getUserId($client, 'test_user');\n        $userId2 = $this->getUserId($client, 'test_user2');\n\n        $client->request('GET', '/users/delete/'.$userId1);\n\n        $this->assertResponseRedirects('/users/');\n        $client->followRedirect();\n\n        $this->assertAnySelectorTextContains('h5', 'Test User 2');\n\n        $client->request('GET', '/users/delete/'.$userId2);\n\n        $this->assertResponseRedirects('/users/');\n        $client->followRedirect();\n\n        $this->assertSelectorTextContains('div#no-user', 'No users yet.');\n    }\n\n    public function testUserDelegates(): void\n    {\n        $user = new AdminUser('admin', 'test');\n\n        $client = static::createClient();\n        $client->loginUser($user);\n\n        $userId = $this->getUserId($client, 'test_user');\n\n        $client->request('GET', '/users/delegates/'.$userId);\n\n        $this->assertResponseIsSuccessful();\n\n        $this->assertSelectorExists('nav.navbar');\n        $this->assertSelectorTextContains('h1', 'Delegates for Test User');\n        $this->assertSelectorTextContains('a.btn', '+ Add a delegate');\n        $this->assertAnySelectorTextContains('div', 'Delegation is enabled for this account.');\n        $this->assertAnySelectorTextContains('a.btn', 'Disable it');\n\n        $client->clickLink('Disable it');\n\n        $this->assertResponseRedirects('/users/delegates/'.$userId);\n        $client->followRedirect();\n\n        $this->assertSelectorExists('nav.navbar');\n        $this->assertSelectorTextContains('h1', 'Delegates for Test User');\n        $this->assertSelectorTextNotContains('a.btn', '+ Add a delegate');\n        $this->assertAnySelectorTextContains('div', 'Delegation is not enabled for this account.');\n        $this->assertAnySelectorTextContains('a.btn', 'Enable it');\n    }\n}\n"
  },
  {
    "path": "tests/Functional/DavTest.php",
    "content": "<?php\n\nnamespace App\\Tests\\Functional;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase;\nuse Symfony\\Component\\BrowserKit\\AbstractBrowser;\n\nclass DavTest extends WebTestCase\n{\n    /**\n     * The DAVController uses a sabre/dav that relies on REQUEST_URI and REQUEST_METHOD\n     * which are not set by default by PHPUnit (or to the wrong values).\n     * We thus force them here so that the request looks like a real one for PHPUnit.\n     */\n    public static function requestDavClient(string $method, string $path): AbstractBrowser\n    {\n        $_SERVER['REQUEST_URI'] = $path;\n        $_SERVER['REQUEST_METHOD'] = $method;\n\n        $client = static::createClient();\n        $client->request($method, $path);\n\n        return $client;\n    }\n\n    public function testUnauthorized(): void\n    {\n        $client = static::requestDavClient('GET', '/dav/');\n\n        $this->assertResponseStatusCodeSame(401);\n    }\n}\n"
  },
  {
    "path": "tests/Functional/Service/BirthdayServiceTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Tests\\Services;\n\nuse App\\Constants;\nuse App\\Entity\\AddressBook;\nuse App\\Entity\\CalendarInstance;\nuse App\\Entity\\CalendarObject;\nuse App\\Entity\\Principal;\nuse App\\Services\\BirthdayService;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Sabre\\CalDAV\\Backend\\PDO as CalendarBackend;\nuse Sabre\\VObject\\Component\\VCalendar;\nuse Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase;\n\nclass BirthdayServiceTest extends KernelTestCase\n{\n    private EntityManagerInterface $em;\n    private BirthdayService $service;\n\n    protected function setUp(): void\n    {\n        self::bootKernel();\n\n        $this->em = static::getContainer()->get(EntityManagerInterface::class);\n        $this->service = static::getContainer()->get(BirthdayService::class);\n\n        $pdo = $this->em->getConnection()->getNativeConnection();\n        $this->service->setBackend(new CalendarBackend($pdo));\n\n        $this->em->getConnection()->beginTransaction();\n    }\n\n    protected function tearDown(): void\n    {\n        $this->em->getConnection()->rollBack();\n        parent::tearDown();\n    }\n\n    private function createAddressBook(\n        string $username = 'testuser',\n        bool $includedInBirthdayCalendar = true,\n    ): AddressBook {\n        $principal = (new Principal())\n            ->setUri(Principal::PREFIX.$username)\n            ->setEmail($username.'@example.com')\n            ->setDisplayName($username);\n        $this->em->persist($principal);\n\n        $addressBook = (new AddressBook())\n            ->setPrincipalUri(Principal::PREFIX.$username)\n            ->setUri('default')\n            ->setDisplayName('Default')\n            ->setDescription('')\n            ->setSynctoken('1')\n            ->setIncludedInBirthdayCalendar($includedInBirthdayCalendar);\n        $this->em->persist($addressBook);\n        $this->em->flush();\n\n        return $addressBook;\n    }\n\n    // -------------------------------------------------------------------------\n    // buildDataFromContact\n    // -------------------------------------------------------------------------\n\n    public function testBuildDataFromContactReturnsNullForEmptyData(): void\n    {\n        $this->assertNull($this->service->buildDataFromContact(''));\n    }\n\n    public function testBuildDataFromContactReturnsNullIfNoBday(): void\n    {\n        $vcard = \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nEND:VCARD\\r\\n\";\n        $this->assertNull($this->service->buildDataFromContact($vcard));\n    }\n\n    public function testBuildDataFromContactReturnsNullIfNoFn(): void\n    {\n        $vcard = \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nBDAY:19900101\\r\\nEND:VCARD\\r\\n\";\n        $this->assertNull($this->service->buildDataFromContact($vcard));\n    }\n\n    public function testBuildDataFromContactReturnsVCalendarWithBday(): void\n    {\n        $vcard = \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\";\n        $result = $this->service->buildDataFromContact($vcard);\n\n        $this->assertInstanceOf(VCalendar::class, $result);\n        $this->assertStringContainsString('John Doe', (string) $result->VEVENT->SUMMARY);\n        $this->assertStringContainsString('1990', (string) $result->VEVENT->SUMMARY);\n        $this->assertEquals('FREQ=YEARLY', (string) $result->VEVENT->RRULE);\n        $this->assertEquals('DATE', (string) $result->VEVENT->DTSTART['VALUE']);\n    }\n\n    public function testBuildDataFromContactHandlesLeapDay(): void\n    {\n        $vcard = \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19920229\\r\\nEND:VCARD\\r\\n\";\n        $result = $this->service->buildDataFromContact($vcard);\n\n        $this->assertInstanceOf(VCalendar::class, $result);\n        $this->assertStringContainsString('BYMONTH=2;BYMONTHDAY=-1', (string) $result->VEVENT->RRULE);\n    }\n\n    public function testBuildDataFromContactHandlesOmitYear(): void\n    {\n        $vcard = \"BEGIN:VCARD\\r\\nVERSION:4.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY;X-APPLE-OMIT-YEAR=1604:16040615\\r\\nEND:VCARD\\r\\n\";\n        $result = $this->service->buildDataFromContact($vcard);\n\n        $this->assertInstanceOf(VCalendar::class, $result);\n        $this->assertStringNotContainsString('(', (string) $result->VEVENT->SUMMARY);\n    }\n\n    public function testBuildDataFromContactAddsAlarm(): void\n    {\n        $vcard = \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\";\n        $result = $this->service->buildDataFromContact($vcard);\n\n        $this->assertNotNull($result->VEVENT->VALARM);\n    }\n\n    // -------------------------------------------------------------------------\n    // birthdayEventChanged\n    // -------------------------------------------------------------------------\n\n    public function testBirthdayEventChangedReturnsFalseWhenSame(): void\n    {\n        $cal = $this->service->buildDataFromContact(\"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\");\n\n        $this->assertFalse($this->service->birthdayEventChanged($cal->serialize(), $cal));\n    }\n\n    public function testBirthdayEventChangedReturnsTrueWhenDifferentDate(): void\n    {\n        $cal1 = $this->service->buildDataFromContact(\"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\");\n        $cal2 = $this->service->buildDataFromContact(\"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900616\\r\\nEND:VCARD\\r\\n\");\n\n        $this->assertTrue($this->service->birthdayEventChanged($cal1->serialize(), $cal2));\n    }\n\n    public function testBirthdayEventChangedReturnsTrueWhenDifferentName(): void\n    {\n        $cal1 = $this->service->buildDataFromContact(\"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\");\n        $cal2 = $this->service->buildDataFromContact(\"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:Jane Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\");\n\n        $this->assertTrue($this->service->birthdayEventChanged($cal1->serialize(), $cal2));\n    }\n\n    public function testBirthdayEventChangedReturnsTrueOnInvalidExistingData(): void\n    {\n        $cal = $this->service->buildDataFromContact(\"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\");\n\n        $this->assertTrue($this->service->birthdayEventChanged('invalid-data', $cal));\n    }\n\n    // -------------------------------------------------------------------------\n    // onCardChanged\n    // -------------------------------------------------------------------------\n\n    public function testOnCardChangedSkipsIfNotIncludedInBirthdayCalendar(): void\n    {\n        $addressBook = $this->createAddressBook(includedInBirthdayCalendar: false);\n\n        $this->service->onCardChanged(\n            $addressBook->getId(),\n            'john.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\"\n        );\n\n        $this->em->clear();\n\n        $object = $this->em->getRepository(CalendarObject::class)->findOneBy(['uri' => 'default-john.vcf.ics']);\n        $this->assertNull($object);\n    }\n\n    public function testOnCardChangedCreatesCalendarObject(): void\n    {\n        $addressBook = $this->createAddressBook();\n\n        $this->service->onCardChanged(\n            $addressBook->getId(),\n            'john.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\"\n        );\n\n        $this->em->clear();\n\n        $object = $this->em->getRepository(CalendarObject::class)->findOneBy(['uri' => 'default-john.vcf.ics']);\n        $this->assertNotNull($object);\n        $this->assertStringContainsString('John Doe', $object->getCalendarData());\n    }\n\n    public function testOnCardChangedUpdatesExistingCalendarObject(): void\n    {\n        $addressBook = $this->createAddressBook();\n\n        $this->service->onCardChanged(\n            $addressBook->getId(),\n            'john.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\"\n        );\n\n        $this->service->onCardChanged(\n            $addressBook->getId(),\n            'john.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Updated\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\"\n        );\n\n        $this->em->clear();\n\n        $object = $this->em->getRepository(CalendarObject::class)->findOneBy(['uri' => 'default-john.vcf.ics']);\n        $this->assertNotNull($object);\n        $this->assertStringContainsString('John Updated', $object->getCalendarData());\n\n        $this->em->clear();\n\n        $instanceBefore = $this->em->getRepository(CalendarInstance::class)->findOneBy([\n            'principalUri' => 'principals/testuser',\n            'uri' => Constants::BIRTHDAY_CALENDAR_URI,\n        ]);\n        $syncTokenBefore = $instanceBefore->getCalendar()->getSynctoken();\n\n        $this->service->onCardChanged(\n            $addressBook->getId(),\n            'john.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Updated Again\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\"\n        );\n\n        $this->em->clear();\n\n        $instanceAfter = $this->em->getRepository(CalendarInstance::class)->findOneBy([\n            'principalUri' => 'principals/testuser',\n            'uri' => Constants::BIRTHDAY_CALENDAR_URI,\n        ]);\n        $this->assertGreaterThan($syncTokenBefore, $instanceAfter->getCalendar()->getSynctoken());\n    }\n\n    public function testOnCardChangedDoesNotCreateObjectIfNoBday(): void\n    {\n        $addressBook = $this->createAddressBook();\n\n        $this->service->onCardChanged(\n            $addressBook->getId(),\n            'john.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nEND:VCARD\\r\\n\"\n        );\n\n        $this->em->clear();\n\n        $object = $this->em->getRepository(CalendarObject::class)->findOneBy(['uri' => 'default-john.vcf.ics']);\n        $this->assertNull($object);\n    }\n\n    // -------------------------------------------------------------------------\n    // onCardDeleted\n    // -------------------------------------------------------------------------\n\n    public function testOnCardDeletedRemovesCalendarObject(): void\n    {\n        $addressBook = $this->createAddressBook();\n\n        $this->service->onCardChanged(\n            $addressBook->getId(),\n            'john.vcf',\n            \"BEGIN:VCARD\\r\\nVERSION:3.0\\r\\nFN:John Doe\\r\\nUID:1234\\r\\nBDAY:19900615\\r\\nEND:VCARD\\r\\n\"\n        );\n\n        $this->service->onCardDeleted($addressBook->getId(), 'john.vcf');\n\n        $this->em->clear();\n\n        $object = $this->em->getRepository(CalendarObject::class)->findOneBy(['uri' => 'default-john.vcf.ics']);\n        $this->assertNull($object);\n    }\n\n    public function testOnCardDeletedIsNoopIfNoCalendarObject(): void\n    {\n        $addressBook = $this->createAddressBook();\n\n        // Should not throw even if no calendar object exists\n        $this->service->onCardDeleted($addressBook->getId(), 'nonexistent.vcf');\n\n        $this->addToAssertionCount(1);\n    }\n\n    // -------------------------------------------------------------------------\n    // ensureBirthdayCalendarExists\n    // -------------------------------------------------------------------------\n\n    public function testEnsureBirthdayCalendarExistsCreatesCalendar(): void\n    {\n        $this->service->ensureBirthdayCalendarExists('principals/testuser');\n\n        $instance = $this->em->getRepository(CalendarInstance::class)->findOneBy([\n            'principalUri' => 'principals/testuser',\n            'uri' => Constants::BIRTHDAY_CALENDAR_URI,\n        ]);\n\n        $this->assertNotNull($instance);\n        $this->assertNotNull($instance->getCalendar());\n    }\n\n    public function testEnsureBirthdayCalendarExistsIsIdempotent(): void\n    {\n        $this->service->ensureBirthdayCalendarExists('principals/testuser');\n        $this->service->ensureBirthdayCalendarExists('principals/testuser');\n\n        $instances = $this->em->getRepository(CalendarInstance::class)->findBy([\n            'principalUri' => 'principals/testuser',\n            'uri' => Constants::BIRTHDAY_CALENDAR_URI,\n        ]);\n\n        $this->assertCount(1, $instances);\n    }\n\n    // -------------------------------------------------------------------------\n    // syncPrincipal\n    // -------------------------------------------------------------------------\n\n    public function testSyncPrincipalDeletesBirthdayCalendarIfNoAddressBooksIncluded(): void\n    {\n        $this->service->ensureBirthdayCalendarExists('principals/testuser');\n        $this->createAddressBook(includedInBirthdayCalendar: false);\n\n        $this->service->syncPrincipal('principals/testuser');\n\n        $instance = $this->em->getRepository(CalendarInstance::class)->findOneBy([\n            'principalUri' => 'principals/testuser',\n            'uri' => Constants::BIRTHDAY_CALENDAR_URI,\n        ]);\n        $this->assertNull($instance);\n    }\n}\n"
  },
  {
    "path": "tests/bootstrap.php",
    "content": "<?php\n\nuse Symfony\\Component\\Dotenv\\Dotenv;\n\nrequire dirname(__DIR__).'/vendor/autoload.php';\n\nif (file_exists(dirname(__DIR__).'/config/bootstrap.php')) {\n    require dirname(__DIR__).'/config/bootstrap.php';\n} elseif (method_exists(Dotenv::class, 'bootEnv')) {\n    (new Dotenv())->bootEnv(dirname(__DIR__).'/.env');\n}\n\n// Create the test database, update the schema and resets the fixture before each test.\n// Note: `--quiet` is needed here for each step so that PHPUnit doesn't fail.\n$actions = [\n    'doctrine:database:create --if-not-exists ',\n    'doctrine:schema:update --complete --force',\n    'doctrine:fixtures:load --no-interaction',\n];\n\nforeach ($actions as $action) {\n    passthru(sprintf(\n        'APP_ENV=%s php \"%s/../bin/console\" %s --quiet',\n        $_ENV['APP_ENV'],\n        __DIR__,\n        $action,\n    ));\n}\n"
  },
  {
    "path": "translations/.gitignore",
    "content": ""
  },
  {
    "path": "translations/messages+intl-icu.de.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file source-language=\"en\" target-language=\"de\" datatype=\"plaintext\" original=\"file.ext\">\n    <header>\n      <tool tool-id=\"symfony\" tool-name=\"Symfony\"/>\n    </header>\n    <body>\n      <trans-unit id=\"poqANwU\" resname=\"davis\">\n        <source>davis</source>\n        <target>Davis</target>\n      </trans-unit>\n      <trans-unit id=\"IRk0vUS\" resname=\"enabled\">\n        <source>enabled</source>\n        <target>aktiviert</target>\n      </trans-unit>\n      <trans-unit id=\"iYiRxKz\" resname=\"disabled\">\n        <source>disabled</source>\n        <target>deaktiviert</target>\n      </trans-unit>\n      <trans-unit id=\"04zDTq_\" resname=\"label.success\">\n        <source>label.success</source>\n        <target>Erfolg</target>\n      </trans-unit>\n      <trans-unit id=\"WcOat7C\" resname=\"label.warning\">\n        <source>label.warning</source>\n        <target>Warnung</target>\n      </trans-unit>\n      <trans-unit id=\"v4zFdy0\" resname=\"label.error\">\n        <source>label.error</source>\n        <target>Fehler</target>\n      </trans-unit>\n      <trans-unit id=\"gyZcKbE\" resname=\"admin.interface\">\n        <source>admin.interface</source>\n        <target>Administrationsoberfläche</target>\n      </trans-unit>\n      <trans-unit id=\"CAiOZVb\" resname=\"close\">\n        <source>close</source>\n        <target>Schließen</target>\n      </trans-unit>\n      <trans-unit id=\"2E3W_Ux\" resname=\"save\">\n        <source>save</source>\n        <target>Speichern</target>\n      </trans-unit>\n      <trans-unit id=\"4IcFUoR\" resname=\"toggle.navigation\">\n        <source>toggle.navigation</source>\n        <target>Navigation umschalten</target>\n      </trans-unit>\n      <trans-unit id=\"IY9tLQj\" resname=\"toggle.theme\">\n        <source>toggle.theme</source>\n        <target>Theme umschalten</target>\n      </trans-unit>\n      <trans-unit id=\"8z17OnR\" resname=\"theme.light\">\n        <source>theme.light</source>\n        <target>Hell</target>\n      </trans-unit>\n      <trans-unit id=\"5ZYx62e\" resname=\"theme.dark\">\n        <source>theme.dark</source>\n        <target>Dunkel</target>\n      </trans-unit>\n      <trans-unit id=\"suizpO6\" resname=\"theme.auto\">\n        <source>theme.auto</source>\n        <target>Auto</target>\n      </trans-unit>\n      <trans-unit id=\"YE7XVBb\" resname=\"title.dashboard\">\n        <source>title.dashboard</source>\n        <target>Dashboard</target>\n      </trans-unit>\n      <trans-unit id=\"G3gS3bJ\" resname=\"title.users_and_resources\">\n        <source>title.users_and_resources</source>\n        <target>Benutzer und Ressourcen</target>\n      </trans-unit>\n      <trans-unit id=\"Ga1B1H3\" resname=\"users.back\">\n        <source>users.back</source>\n        <target>Zurück zu Benutzer</target>\n      </trans-unit>\n      <trans-unit id=\"2Mx633A\" resname=\"addressbooks.new\">\n        <source>addressbooks.new</source>\n        <target>Neues Adressbuch</target>\n      </trans-unit>\n      <trans-unit id=\"ddUyfGF\" resname=\"calendars.new\">\n        <source>calendars.new</source>\n        <target>Neuer Kalender</target>\n      </trans-unit>\n      <trans-unit id=\"aCxiLZi\" resname=\"calendars.uri\">\n        <source>calendars.uri</source>\n        <target>URI</target>\n      </trans-unit>\n      <trans-unit id=\"JPI5wA3\" resname=\"users.new\">\n        <source>users.new</source>\n        <target>Neuer Benutzer</target>\n      </trans-unit>\n      <trans-unit id=\"weOCfT2\" resname=\"dashboard.env\">\n        <source>dashboard.env</source>\n        <target>Konfigurierte Arbeitsumgebung</target>\n      </trans-unit>\n      <trans-unit id=\"wx8fUKV\" resname=\"dashboard.capabilities\">\n        <source>dashboard.capabilities</source>\n        <target>Funktionen</target>\n      </trans-unit>\n      <trans-unit id=\"zJSQAI8\" resname=\"dashboard.objects\">\n        <source>dashboard.objects</source>\n        <target>Objekte</target>\n      </trans-unit>\n      <trans-unit id=\"mgKUnX3\" resname=\"dashboard.version\">\n        <source>dashboard.version</source>\n        <target>Version</target>\n      </trans-unit>\n      <trans-unit id=\"MpsZ_kY\" resname=\"dashboard.auth\">\n        <source>dashboard.auth</source>\n        <target>Authentifizierung</target>\n      </trans-unit>\n      <trans-unit id=\"jNecvkm\" resname=\"dashboard.auth_realm\">\n        <source>dashboard.auth_realm</source>\n        <target>Realm</target>\n      </trans-unit>\n      <trans-unit id=\"Utr1VRG\" resname=\"dashboard.invite_from_address\">\n        <source>dashboard.invite_from_address</source>\n        <target>Aus Adressbuch einladen</target>\n      </trans-unit>\n      <trans-unit id=\"6A.HMvX\" resname=\"dashboard.server_timezone\">\n        <source>dashboard.server_timezone</source>\n        <target>Server (PHP) Timezone</target>\n      </trans-unit>\n      <trans-unit id=\"mD_kv2n\" resname=\"dashboard.bad_timezone_configuration\">\n        <source>dashboard.bad_timezone_configuration</source>\n        <target>Falsche Konfiguration der Zeitzone env var</target>\n      </trans-unit>\n      <trans-unit id=\"3jV45P0\" resname=\"dashboard.no_timezone_configuration\">\n        <source>dashboard.no_timezone_configuration</source>\n        <target>Zeitzone wird vom Server nicht erzwungen</target>\n      </trans-unit>\n      <trans-unit id=\"BDynkV_\" resname=\"dashboard.users\">\n        <source>dashboard.users</source>\n        <target>Benutzer</target>\n      </trans-unit>\n      <trans-unit id=\"oAGwPD5\" resname=\"dashboard.calendars\">\n        <source>dashboard.calendars</source>\n        <target>Kalender</target>\n      </trans-unit>\n      <trans-unit id=\"79cIq7y\" resname=\"dashboard.calendars.help\">\n        <source>dashboard.calendars.help</source>\n        <target>(inkl. Erinnerungslisten)</target>\n      </trans-unit>\n      <trans-unit id=\"5SShPEM\" resname=\"dashboard.events\">\n        <source>dashboard.events</source>\n        <target>Ereignisse</target>\n      </trans-unit>\n      <trans-unit id=\"kl9QvVx\" resname=\"dashboard.address_books\">\n        <source>dashboard.address_books</source>\n        <target>Adressbücher</target>\n      </trans-unit>\n      <trans-unit id=\"OfamhPy\" resname=\"dashboard.contacts\">\n        <source>dashboard.contacts</source>\n        <target>Kontakte</target>\n      </trans-unit>\n      <trans-unit id=\"iQm_wg0\" resname=\"addressbooks.edit\">\n        <source>addressbooks.edit</source>\n        <target>Adressbuch «{name}» bearbeiten</target>\n      </trans-unit>\n      <trans-unit id=\"BVAU8Tv\" resname=\"edit\">\n        <source>edit</source>\n        <target>Bearbeiten</target>\n      </trans-unit>\n      <trans-unit id=\"JmmK.9X\" resname=\"add\">\n        <source>add</source>\n        <target>Hinzufügen</target>\n      </trans-unit>\n      <trans-unit id=\"u1x_1qs\" resname=\"delete\">\n        <source>delete</source>\n        <target>Löschen</target>\n      </trans-unit>\n      <trans-unit id=\"lsvVz5G\" resname=\"calendars.edit\">\n        <source>calendars.edit</source>\n        <target>Kalender «{name}» bearbeiten</target>\n      </trans-unit>\n      <trans-unit id=\"1kNwhhb\" resname=\"calendars.entries\">\n        <source>calendars.entries</source>\n        <target>{count, plural, =0 {Kein Eintrag} one {Ein Eintrag} other {# Einträge}}</target>\n      </trans-unit>\n      <trans-unit id=\"inLkptW\" resname=\"calendars.component.events\">\n        <source>calendars.component.events</source>\n        <target>Ereignisse</target>\n      </trans-unit>\n      <trans-unit id=\"m1s.bXE\" resname=\"calendars.component.todos\">\n        <source>calendars.component.todos</source>\n        <target>Aufgaben</target>\n      </trans-unit>\n      <trans-unit id=\"Mx8PqQR\" resname=\"calendars.setup.title\">\n        <source>calendars.setup.title</source>\n        <target>Einrichtungsinformation</target>\n      </trans-unit>\n      <trans-unit id=\"jA5BLYc\" resname=\"calendars.component.notes\">\n        <source>calendars.component.notes</source>\n        <target>Notizen</target>\n      </trans-unit>\n      <trans-unit id=\"O2IyoII\" resname=\"users.edit\">\n        <source>users.edit</source>\n        <target>Benutzer «{username}» bearbeiten</target>\n      </trans-unit>\n      <trans-unit id=\"NPrwvQ_\" resname=\"addressbooks.for\">\n        <source>addressbooks.for</source>\n        <target>Adressbuch von {who}</target>\n      </trans-unit>\n      <trans-unit id=\".ri3V4T\" resname=\"addressbooks.uri\">\n        <source>addressbooks.uri</source>\n        <target>Adressbuch URI</target>\n      </trans-unit>\n      <trans-unit id=\"miq7_xK\" resname=\"addressbooks.contacts\">\n        <source>addressbooks.contacts</source>\n        <target>{count, plural, =0 {Kein Kontakt} one {Ein Kontakt} other {# Kontakte}}</target>\n      </trans-unit>\n      <trans-unit id=\"CqvStpa\" resname=\"calendars.for\">\n        <source>calendars.for</source>\n        <target>Kalender für {who}</target>\n      </trans-unit>\n      <trans-unit id=\"ybqHGGr\" resname=\"calendars.shared.with\">\n        <source>calendars.shared.with</source>\n        <target>Kalender geteilt mit {who}</target>\n      </trans-unit>\n      <trans-unit id=\"caEhjpg\" resname=\"users.username\">\n        <source>users.username</source>\n        <target>Benutzername</target>\n      </trans-unit>\n      <trans-unit id=\"kWHR7Qt\" resname=\"users.uri\">\n        <source>users.uri</source>\n        <target>Principal URI</target>\n      </trans-unit>\n      <trans-unit id=\"rD9wqzu\" resname=\"users.administrator\">\n        <source>users.administrator</source>\n        <target>Administrator</target>\n      </trans-unit>\n      <trans-unit id=\"tDUC2FT\" resname=\"users.calendars\">\n        <source>users.calendars</source>\n        <target>Kalender</target>\n      </trans-unit>\n      <trans-unit id=\"vrTk17r\" resname=\"users.addressbooks\">\n        <source>users.addressbooks</source>\n        <target>Adressbücher</target>\n      </trans-unit>\n      <trans-unit id=\"KB__Nh8\" resname=\"default.calendar.title\">\n        <source>default.calendar.title</source>\n        <target>Standard-Kalender</target>\n      </trans-unit>\n      <trans-unit id=\"ORuYu1p\" resname=\"default.calendar.description\">\n        <source>default.calendar.description</source>\n        <target>Standard-Kalender für {user}</target>\n      </trans-unit>\n      <trans-unit id=\"BV0O97e\" resname=\"default.addressbook.title\">\n        <source>default.addressbook.title</source>\n        <target>Standard-Adressbuch</target>\n      </trans-unit>\n      <trans-unit id=\"UKZXrbn\" resname=\"default.addressbook.description\">\n        <source>default.addressbook.description</source>\n        <target>Standard-Adressbuch für {user}</target>\n      </trans-unit>\n      <trans-unit id=\"4jEMVVj\" resname=\"user.saved\">\n        <source>user.saved</source>\n        <target>Benutzer erfolgreich gespeichert</target>\n      </trans-unit>\n      <trans-unit id=\"j1XlCAb\" resname=\"user.deleted\">\n        <source>user.deleted</source>\n        <target>Benutzer erfolgreich gelöscht</target>\n      </trans-unit>\n      <trans-unit id=\"rey4Kec\" resname=\"calendar.saved\">\n        <source>calendar.saved</source>\n        <target>Kalender erfolgreich gespeichert</target>\n      </trans-unit>\n      <trans-unit id=\"UkuOpBs\" resname=\"calendar.deleted\">\n        <source>calendar.deleted</source>\n        <target>Kalender erfolgreich gelöscht</target>\n      </trans-unit>\n      <trans-unit id=\"nLmfYXY\" resname=\"calendar.shared\">\n        <source>calendar.shared</source>\n        <target>Geteilten Kalender erfolgreich bearbeitet</target>\n      </trans-unit>\n      <trans-unit id=\"a7D0zuw\" resname=\"calendar.revoked\">\n        <source>calendar.revoked</source>\n        <target>Geteilten Kalender erfolgreich zurückgezogen</target>\n      </trans-unit>\n      <trans-unit id=\"pL5FhyS\" resname=\"calendars.delegates.member.add\">\n        <source>calendars.delegates.member.add</source>\n        <target>Diesen Kalender mit anderen Benutzern teilen:</target>\n      </trans-unit>\n      <trans-unit id=\"gdXa6Lc\" resname=\"calendars.delegates.member.help\">\n        <source>calendars.delegates.member.help</source>\n        <target>Das Hinzufügen eines Benutzers, der bereits einen gemeinsamen Zugriff auf diesen Kalender hat, wirkt sich nur auf dessen Zugriffsrecht aus.</target>\n      </trans-unit>\n      <trans-unit id=\"zWt8oG0\" resname=\"calendars.delegates.member.none\">\n        <source>calendars.delegates.member.none</source>\n        <target>Es gibt keine anderen Benutzer, mit denen Sie diesen Kalender teilen können.</target>\n      </trans-unit>\n      <trans-unit id=\"ghe05Yl\" resname=\"addressbooks.saved\">\n        <source>addressbooks.saved</source>\n        <target>Adressbuch erfolgreich gespeichert</target>\n      </trans-unit>\n      <trans-unit id=\"SQckZXJ\" resname=\"addressbooks.deleted\">\n        <source>addressbooks.deleted</source>\n        <target>Adressbuch erfolgreich gelöscht</target>\n      </trans-unit>\n      <trans-unit id=\"YcMyNwC\" resname=\"addressbooks.back\">\n        <source>addressbooks.back</source>\n        <target>Zurück zu Adressbücher für {user}</target>\n      </trans-unit>\n      <trans-unit id=\"VgrgSjx\" resname=\"calendars.back\">\n        <source>calendars.back</source>\n        <target>Zurück zu Kalender für {user}</target>\n      </trans-unit>\n      <trans-unit id=\"xNkdpFb\" resname=\"form.password.empty\">\n        <source>form.password.empty</source>\n        <target>Leer lassen, wenn Sie es nicht ändern wollen.</target>\n      </trans-unit>\n      <trans-unit id=\"2wU1UVt\" resname=\"form.password.match\">\n        <source>form.password.match</source>\n        <target>Das Passwortfeld muss übereinstimmen.</target>\n      </trans-unit>\n      <trans-unit id=\"r2fjRrB\" resname=\"form.password\">\n        <source>form.password</source>\n        <target>Passwort</target>\n      </trans-unit>\n      <trans-unit id=\".EvZY.m\" resname=\"form.password.repeat\">\n        <source>form.password.repeat</source>\n        <target>Passwort wiederholen</target>\n      </trans-unit>\n      <trans-unit id=\"vXLetA0\" resname=\"form.email\">\n        <source>form.email</source>\n        <target>E-Mail</target>\n      </trans-unit>\n      <trans-unit id=\"RQWeqAY\" resname=\"form.admin\">\n        <source>form.admin</source>\n        <target>Ist dieser Benutzer ein Administrator?</target>\n      </trans-unit>\n      <trans-unit id=\"sLIuuSU\" resname=\"form.admin.help\">\n        <source>form.admin.help</source>\n        <target>Wenn diese Option aktiviert ist, wird dieser Benutzer (d. h. sein Hauptbenutzer) in jede einzelne ACL-Regel mit den Berechtigungen \"{DAV:}all\" aufgenommen.</target>\n      </trans-unit>\n      <trans-unit id=\"nXCRwB3\" resname=\"form.displayName\">\n        <source>form.displayName</source>\n        <target>Anzeigename</target>\n      </trans-unit>\n      <trans-unit id=\"2_y0ttv\" resname=\"form.username\">\n        <source>form.username</source>\n        <target>Benutzername</target>\n      </trans-unit>\n      <trans-unit id=\"iicAzGB\" resname=\"form.username.help\">\n        <source>form.username.help</source>\n        <target>Kann eine E-Mail sein, aber nicht zwingend.</target>\n      </trans-unit>\n      <trans-unit id=\"IHOaweC\" resname=\"form.events\">\n        <source>form.events</source>\n        <target>Ereignisse</target>\n      </trans-unit>\n      <trans-unit id=\"4pbkJcC\" resname=\"form.notes\">\n        <source>form.notes</source>\n        <target>Notizen</target>\n      </trans-unit>\n      <trans-unit id=\"dhJC0.u\" resname=\"form.todos\">\n        <source>form.todos</source>\n        <target>Aufgaben</target>\n      </trans-unit>\n      <trans-unit id=\"ku2Vzal\" resname=\"form.uri\">\n        <source>form.uri</source>\n        <target>URI</target>\n      </trans-unit>\n      <trans-unit id=\"PHZs.1x\" resname=\"form.includedInBirthdayCalendar\">\n        <source>form.includedInBirthdayCalendar</source>\n        <target>Im Geburtstagskalender enthalten?</target>\n      </trans-unit>\n      <trans-unit id=\"GBcNXE.\" resname=\"form.description\">\n        <source>form.description</source>\n        <target>Beschreibung</target>\n      </trans-unit>\n      <trans-unit id=\"r_YZYfq\" resname=\"form.color\">\n        <source>form.color</source>\n        <target>Kalenderfarbe</target>\n      </trans-unit>\n      <trans-unit id=\"IPhqrmQ\" resname=\"form.public\">\n        <source>form.public</source>\n        <target>Öffentlich</target>\n      </trans-unit>\n      <trans-unit id=\"e02Cnv6\" resname=\"form.name.help.carddav\">\n        <source>form.name.help.carddav</source>\n        <target>Dieser Name wird in Ihrem CardDAV-Client angezeigt.</target>\n      </trans-unit>\n      <trans-unit id=\"coqCyJL\" resname=\"form.includedInBirthdayCalendar.help\">\n        <source>form.includedInBirthdayCalendar.help</source>\n        <target>Wenn diese Option ausgewählt ist, werden alle Karten mit einem gültigen Geburtstag in den Geburtstagskalender des Hauptbenutzers aufgenommen, der als freigegebener Kalender in Ihrem Konto verfügbar ist.</target>\n      </trans-unit>\n      <trans-unit id=\"rqpkW3X\" resname=\"form.name.help.caldav\">\n        <source>form.name.help.caldav</source>\n        <target>Dieser Name wird in Ihrem CalDAV-Client angezeigt</target>\n      </trans-unit>\n      <trans-unit id=\"XcmEaP5\" resname=\"form.uri.help.carddav\">\n        <source>form.uri.help.carddav</source>\n        <target>Dies ist der eindeutige Bezeichner für dieses Adressbuch. Erlaubte Zeichen sind Ziffern, Kleinbuchstaben und das Bindestrichsymbol '-'.</target>\n      </trans-unit>\n      <trans-unit id=\"reeyEAg\" resname=\"form.uri.help.caldav\">\n        <source>form.uri.help.caldav</source>\n        <target>Dies ist der eindeutige Bezeichner für dieses Kalender. Erlaubte Zeichen sind Ziffern, Kleinbuchstaben und das Bindestrichsymbol '-'.</target>\n      </trans-unit>\n      <trans-unit id=\"xAoSnrW\" resname=\"form.public.help.caldav\">\n        <source>form.public.help.caldav</source>\n        <target>Wenn der Kalender öffentlich ist, ist er für jeden, der den Link kennt, zugänglich.</target>\n      </trans-unit>\n      <trans-unit id=\"OqHbbck\" resname=\"form.color.help\">\n        <source>form.color.help</source>\n        <target>Das ist die Farbe, die in Ihrem CalDAV-Client angezeigt werden soll. Sie muss im Format „#RRGGBBAA“ (Alphakanal ist optional) mit Hexadezimalwerten angegeben werden. Dieser Wert ist optional.</target>\n      </trans-unit>\n      <trans-unit id=\"ie6kT2V\" resname=\"form.events.help\">\n        <source>form.events.help</source>\n        <target>Wenn diese Option markiert ist, werden Ereignisse in diesem Kalender aktiviert. Sie werden in den Kalender-Clients angezeigt.</target>\n      </trans-unit>\n      <trans-unit id=\"m2d03h7\" resname=\"form.todos.help\">\n        <source>form.todos.help</source>\n        <target>Wenn diese Option aktiviert ist, werden Aufgaben in diesem Kalender aktiviert. Sie werden in Erinnerungsprogrammen angezeigt (z. B. macOS Erinnerungen App).</target>\n      </trans-unit>\n      <trans-unit id=\"DZBm7go\" resname=\"form.notes.help\">\n        <source>form.notes.help</source>\n        <target>Wenn diese Option markiert ist, werden Notizen in diesem Kalender aktiviert.</target>\n      </trans-unit>\n      <trans-unit id=\"nmAIJpy\" resname=\"addressbooks.modal.title\">\n        <source>addressbooks.modal.title</source>\n        <target>Dieses Adressbuch löschen?</target>\n      </trans-unit>\n      <trans-unit id=\"3mPrY9E\" resname=\"addressbooks.modal.text\">\n        <source>addressbooks.modal.text</source>\n        <target>Sind Sie sicher, dass Sie dieses Adressbuch löschen möchten? Alle darin enthaltenen Kontakte werden dann ebenfalls gelöscht.</target>\n      </trans-unit>\n      <trans-unit id=\"YmO7Wcu\" resname=\"calendars.modal.title\">\n        <source>calendars.modal.title</source>\n        <target>Diesen Kalender löschen?</target>\n      </trans-unit>\n      <trans-unit id=\"XM0TWcI\" resname=\"calendars.modal.text\">\n        <source>calendars.modal.text</source>\n        <target>Sind Sie sicher, dass Sie diesen Kalender löschen möchten? Alle darin enthaltenen Ereignisse, Aufgaben und Notizen werden ebenfalls gelöscht.</target>\n      </trans-unit>\n      <trans-unit id=\"3Own8oC\" resname=\"revoke.modal.title\">\n        <source>revoke.modal.title</source>\n        <target>Zugriff auf diesen Kalender widerrufen?</target>\n      </trans-unit>\n      <trans-unit id=\"N57f0XR\" resname=\"revoke.modal.text\">\n        <source>revoke.modal.text</source>\n        <target>Sind Sie sicher, dass Sie den Zugang zu diesem Kalender widerrufen möchten? Der Benutzer verliert den Zugriff auf alle Ereignisse, Aufgaben und Notizen. Der ursprüngliche Kalender wird nicht gelöscht.</target>\n      </trans-unit>\n      <trans-unit id=\"YVgPRJT\" resname=\"users.modal.title\">\n        <source>users.modal.title</source>\n        <target>Diesen Benutzer löschen?</target>\n      </trans-unit>\n      <trans-unit id=\"TBZP8bg\" resname=\"users.modal.text\">\n        <source>users.modal.text</source>\n        <target>Sind Sie sicher, dass Sie diesen Benutzer löschen möchten? Alle zugehörigen Kalender und Adressbücher werden ebenfalls gelöscht.</target>\n      </trans-unit>\n      <trans-unit id=\"lyTSshw\" resname=\"delegates.modal.title\">\n        <source>delegates.modal.title</source>\n        <target>Diesen Vertreter löschen?</target>\n      </trans-unit>\n      <trans-unit id=\"MT5Ijud\" resname=\"delegates.modal.text\">\n        <source>delegates.modal.text</source>\n        <target>Sind Sie sicher, dass Sie diesen Vertreter entfernen möchten? Dieser Benutzer hat dann keinen Zugang mehr zu den Kalendern, Kontakten usw.</target>\n      </trans-unit>\n      <trans-unit id=\"0BNEzcg\" resname=\"delegates.member.help\">\n        <source>delegates.member.help</source>\n        <target>Das Hinzufügen eines Benutzers, der bereits Vertreter ist, wirkt sich nur auf sein Zugriffsrecht aus.</target>\n      </trans-unit>\n      <trans-unit id=\"WmbkJDd\" resname=\"cancel\">\n        <source>cancel</source>\n        <target>Abbrechen</target>\n      </trans-unit>\n      <trans-unit id=\"KoqKM6m\" resname=\"yes\">\n        <source>yes</source>\n        <target>Ja</target>\n      </trans-unit>\n      <trans-unit id=\"e3XmWtO\" resname=\"no\">\n        <source>no</source>\n        <target>Nein</target>\n      </trans-unit>\n      <trans-unit id=\"d7yLLjA\" resname=\"login.signin\">\n        <source>login.signin</source>\n        <target>Bitte anmelden</target>\n      </trans-unit>\n      <trans-unit id=\"4TTdn8M\" resname=\"login.username\">\n        <source>login.username</source>\n        <target>Benutzername</target>\n      </trans-unit>\n      <trans-unit id=\"sp0KUC6\" resname=\"login.password\">\n        <source>login.password</source>\n        <target>Passwort</target>\n      </trans-unit>\n      <trans-unit id=\"8jf9lcX\" resname=\"login.submit\">\n        <source>login.submit</source>\n        <target>Senden</target>\n      </trans-unit>\n      <trans-unit id=\"F55nsy3\" resname=\"logout\">\n        <source>logout</source>\n        <target>Abmelden</target>\n      </trans-unit>\n      <trans-unit id=\"GPvgh0L\" resname=\"login.already\">\n        <source>login.already</source>\n        <target>Sie sind bereits als {Benutzername} eingeloggt.</target>\n      </trans-unit>\n      <trans-unit id=\"RcIhyKp\" resname=\"no.users.yet\">\n        <source>no.users.yet</source>\n        <target>Noch keine Benutzer.</target>\n      </trans-unit>\n      <trans-unit id=\"P5zs.4x\" resname=\"remove\">\n        <source>remove</source>\n        <target>Entfernen</target>\n      </trans-unit>\n      <trans-unit id=\"4HlJBp8\" resname=\"revoke\">\n        <source>revoke</source>\n        <target>Zurückziehen</target>\n      </trans-unit>\n      <trans-unit id=\"SXq.3tZ\" resname=\"sharing\">\n        <source>sharing</source>\n        <target>Teilen</target>\n      </trans-unit>\n      <trans-unit id=\"elrbasT\" resname=\"calendars.sharing\">\n        <source>calendars.sharing</source>\n        <target>Gemeinsame Nutzung von Kalendern</target>\n      </trans-unit>\n      <trans-unit id=\"HG_zUPj\" resname=\"calendars.delegates.add\">\n        <source>calendars.delegates.add</source>\n        <target>Vertreter hinzufügen</target>\n      </trans-unit>\n      <trans-unit id=\"CIm3U7z\" resname=\"calendars.delegates.for\">\n        <source>calendars.delegates.for</source>\n        <target>Vertreter für {what}</target>\n      </trans-unit>\n      <trans-unit id=\"0MBan0c\" resname=\"calendars.delegates.new\">\n        <source>calendars.delegates.new</source>\n        <target>Neuer Vertreter</target>\n      </trans-unit>\n      <trans-unit id=\"P0vGXfx\" resname=\"calendars.delegates.existing\">\n        <source>calendars.delegates.existing</source>\n        <target>Dieser Kalender wird geteilt mit:</target>\n      </trans-unit>\n      <trans-unit id=\"0qEUvpG\" resname=\"calendars.delegates.none\">\n        <source>calendars.delegates.none</source>\n        <target>keine</target>\n      </trans-unit>\n      <trans-unit id=\"p6swGey\" resname=\"calendars.delegates.member\">\n        <source>calendars.delegates.member</source>\n        <target>Mitglied:</target>\n      </trans-unit>\n      <trans-unit id=\"sIayt9k\" resname=\"calendars.delegates.write.give\">\n        <source>calendars.delegates.write.give</source>\n        <target>Schreibzugriff gewähren?</target>\n      </trans-unit>\n      <trans-unit id=\"oxkIwfV\" resname=\"users.delegates\">\n        <source>users.delegates</source>\n        <target>Vertreter</target>\n      </trans-unit>\n      <trans-unit id=\"q.VJSGh\" resname=\"delegates.enabled.text\">\n        <source>delegates.enabled.text</source>\n        <target>Die Vertreterfunktion für dieses Konto ist aktiviert.</target>\n      </trans-unit>\n      <trans-unit id=\"42j2tRB\" resname=\"delegates.disable.warning\">\n        <source>delegates.disable.warning</source>\n        <target>⚠ Wenn Sie diese Funktion deaktivieren, verlieren alle Ihre Vertreter den Zugang zu diesem Kalender.</target>\n      </trans-unit>\n      <trans-unit id=\"HRSK3Wi\" resname=\"delegates.disable\">\n        <source>delegates.disable</source>\n        <target>Deaktivieren</target>\n      </trans-unit>\n      <trans-unit id=\"KHtTcdj\" resname=\"delegates.disabled.text\">\n        <source>delegates.disabled.text</source>\n        <target>Die Vertreter-Funktion ist für dieses Konto deaktiviert.</target>\n      </trans-unit>\n      <trans-unit id=\"bVvwuc_\" resname=\"delegates.enable\">\n        <source>delegates.enable</source>\n        <target>Aktivieren</target>\n      </trans-unit>\n      <trans-unit id=\"Au1oBii\" resname=\"delegates.write\">\n        <source>delegates.write</source>\n        <target>hat Schreibzugriff</target>\n      </trans-unit>\n      <trans-unit id=\"R7lqMDG\" resname=\"delegates.readonly\">\n        <source>delegates.readonly</source>\n        <target>hat Lesezugriff</target>\n      </trans-unit>\n      <trans-unit id=\"TD5.lT1\" resname=\"calendar.share_access.2\">\n        <source>calendar.share_access.2</source>\n        <target>schreibgeschützt</target>\n      </trans-unit>\n      <trans-unit id=\"wZOPHI8\" resname=\"calendar.share_access.3\">\n        <source>calendar.share_access.3</source>\n        <target>lesen / schreiben</target>\n      </trans-unit>\n      <trans-unit id=\"F8h2YaT\" resname=\"calendar.public\">\n        <source>calendar.public</source>\n        <target>öffentlich</target>\n      </trans-unit>\n      <trans-unit id=\"mYDsetM\" resname=\"calendar.auto\">\n        <source>calendar.auto</source>\n        <target>auto</target>\n      </trans-unit>\n      <trans-unit id=\"FSDg5q6\" resname=\"calendar.subscription\">\n        <source>calendar.subscription</source>\n        <target>Abonnement</target>\n      </trans-unit>\n      <trans-unit id=\"mJ_hMaA\" resname=\"calendars.subscriptions\">\n        <source>calendars.subscriptions</source>\n        <target>Abonnements</target>\n      </trans-unit>\n      <trans-unit id=\"ZnEWE47\" resname=\"calendars.auto\">\n        <source>calendars.auto</source>\n        <target>Automatisch generiert</target>\n      </trans-unit>\n    </body>\n  </file>\n</xliff>\n"
  },
  {
    "path": "translations/messages+intl-icu.en.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file source-language=\"en\" target-language=\"en\" datatype=\"plaintext\" original=\"file.ext\">\n    <header>\n      <tool tool-id=\"symfony\" tool-name=\"Symfony\"/>\n    </header>\n    <body>\n      <trans-unit id=\"poqANwU\" resname=\"davis\">\n        <source>davis</source>\n        <target>Davis</target>\n      </trans-unit>\n      <trans-unit id=\"IRk0vUS\" resname=\"enabled\">\n        <source>enabled</source>\n        <target>enabled</target>\n      </trans-unit>\n      <trans-unit id=\"iYiRxKz\" resname=\"disabled\">\n        <source>disabled</source>\n        <target>disabled</target>\n      </trans-unit>\n      <trans-unit id=\"04zDTq_\" resname=\"label.success\">\n        <source>label.success</source>\n        <target>Success</target>\n      </trans-unit>\n      <trans-unit id=\"WcOat7C\" resname=\"label.warning\">\n        <source>label.warning</source>\n        <target>Warning</target>\n      </trans-unit>\n      <trans-unit id=\"v4zFdy0\" resname=\"label.error\">\n        <source>label.error</source>\n        <target>Error</target>\n      </trans-unit>\n      <trans-unit id=\"gyZcKbE\" resname=\"admin.interface\">\n        <source>admin.interface</source>\n        <target>Administration interface</target>\n      </trans-unit>\n      <trans-unit id=\"CAiOZVb\" resname=\"close\">\n        <source>close</source>\n        <target>Close</target>\n      </trans-unit>\n      <trans-unit id=\"2E3W_Ux\" resname=\"save\">\n        <source>save</source>\n        <target>Save</target>\n      </trans-unit>\n      <trans-unit id=\"4IcFUoR\" resname=\"toggle.navigation\">\n        <source>toggle.navigation</source>\n        <target>Toggle navigation</target>\n      </trans-unit>\n      <trans-unit id=\"IY9tLQj\" resname=\"toggle.theme\">\n        <source>toggle.theme</source>\n        <target>Toggle theme</target>\n      </trans-unit>\n      <trans-unit id=\"8z17OnR\" resname=\"theme.light\">\n        <source>theme.light</source>\n        <target>Light</target>\n      </trans-unit>\n      <trans-unit id=\"5ZYx62e\" resname=\"theme.dark\">\n        <source>theme.dark</source>\n        <target>Dark</target>\n      </trans-unit>\n      <trans-unit id=\"suizpO6\" resname=\"theme.auto\">\n        <source>theme.auto</source>\n        <target>Auto</target>\n      </trans-unit>\n      <trans-unit id=\"YE7XVBb\" resname=\"title.dashboard\">\n        <source>title.dashboard</source>\n        <target>Dashboard</target>\n      </trans-unit>\n      <trans-unit id=\"G3gS3bJ\" resname=\"title.users_and_resources\">\n        <source>title.users_and_resources</source>\n        <target>Users and Resources</target>\n      </trans-unit>\n      <trans-unit id=\"Ga1B1H3\" resname=\"users.back\">\n        <source>users.back</source>\n        <target>Back to Users</target>\n      </trans-unit>\n      <trans-unit id=\"2Mx633A\" resname=\"addressbooks.new\">\n        <source>addressbooks.new</source>\n        <target>New Address Book</target>\n      </trans-unit>\n      <trans-unit id=\"ddUyfGF\" resname=\"calendars.new\">\n        <source>calendars.new</source>\n        <target>New Calendar</target>\n      </trans-unit>\n      <trans-unit id=\"aCxiLZi\" resname=\"calendars.uri\">\n        <source>calendars.uri</source>\n        <target>URI</target>\n      </trans-unit>\n      <trans-unit id=\"JPI5wA3\" resname=\"users.new\">\n        <source>users.new</source>\n        <target>New User</target>\n      </trans-unit>\n      <trans-unit id=\"weOCfT2\" resname=\"dashboard.env\">\n        <source>dashboard.env</source>\n        <target>Configured environment</target>\n      </trans-unit>\n      <trans-unit id=\"wx8fUKV\" resname=\"dashboard.capabilities\">\n        <source>dashboard.capabilities</source>\n        <target>Capabilities</target>\n      </trans-unit>\n      <trans-unit id=\"zJSQAI8\" resname=\"dashboard.objects\">\n        <source>dashboard.objects</source>\n        <target>Objects</target>\n      </trans-unit>\n      <trans-unit id=\"mgKUnX3\" resname=\"dashboard.version\">\n        <source>dashboard.version</source>\n        <target>Version</target>\n      </trans-unit>\n      <trans-unit id=\"MpsZ_kY\" resname=\"dashboard.auth\">\n        <source>dashboard.auth</source>\n        <target>Auth</target>\n      </trans-unit>\n      <trans-unit id=\"jNecvkm\" resname=\"dashboard.auth_realm\">\n        <source>dashboard.auth_realm</source>\n        <target>Realm</target>\n      </trans-unit>\n      <trans-unit id=\"Utr1VRG\" resname=\"dashboard.invite_from_address\">\n        <source>dashboard.invite_from_address</source>\n        <target>Invite From Address</target>\n      </trans-unit>\n      <trans-unit id=\"6A.HMvX\" resname=\"dashboard.server_timezone\">\n        <source>dashboard.server_timezone</source>\n        <target>App (PHP) Timezone</target>\n      </trans-unit>\n      <trans-unit id=\"mD_kv2n\" resname=\"dashboard.bad_timezone_configuration\">\n        <source>dashboard.bad_timezone_configuration</source>\n        <target>Bad timezone configuration env var</target>\n      </trans-unit>\n      <trans-unit id=\"3jV45P0\" resname=\"dashboard.no_timezone_configuration\">\n        <source>dashboard.no_timezone_configuration</source>\n        <target>Timezone not enforced by app</target>\n      </trans-unit>\n      <trans-unit id=\"BDynkV_\" resname=\"dashboard.users\">\n        <source>dashboard.users</source>\n        <target>Users</target>\n      </trans-unit>\n      <trans-unit id=\"oAGwPD5\" resname=\"dashboard.calendars\">\n        <source>dashboard.calendars</source>\n        <target>Calendars</target>\n      </trans-unit>\n      <trans-unit id=\"79cIq7y\" resname=\"dashboard.calendars.help\">\n        <source>dashboard.calendars.help</source>\n        <target>(incl. Reminders lists)</target>\n      </trans-unit>\n      <trans-unit id=\"5SShPEM\" resname=\"dashboard.events\">\n        <source>dashboard.events</source>\n        <target>Events</target>\n      </trans-unit>\n      <trans-unit id=\"kl9QvVx\" resname=\"dashboard.address_books\">\n        <source>dashboard.address_books</source>\n        <target>Address Books</target>\n      </trans-unit>\n      <trans-unit id=\"OfamhPy\" resname=\"dashboard.contacts\">\n        <source>dashboard.contacts</source>\n        <target>Contacts</target>\n      </trans-unit>\n      <trans-unit id=\"iQm_wg0\" resname=\"addressbooks.edit\">\n        <source>addressbooks.edit</source>\n        <target>Editing Address Book «{name}»</target>\n      </trans-unit>\n      <trans-unit id=\"BVAU8Tv\" resname=\"edit\">\n        <source>edit</source>\n        <target>Edit</target>\n      </trans-unit>\n      <trans-unit id=\"JmmK.9X\" resname=\"add\">\n        <source>add</source>\n        <target>Add</target>\n      </trans-unit>\n      <trans-unit id=\"u1x_1qs\" resname=\"delete\">\n        <source>delete</source>\n        <target>Delete</target>\n      </trans-unit>\n      <trans-unit id=\"lsvVz5G\" resname=\"calendars.edit\">\n        <source>calendars.edit</source>\n        <target>Editing Calendar «{name}»</target>\n      </trans-unit>\n      <trans-unit id=\"1kNwhhb\" resname=\"calendars.entries\">\n        <source>calendars.entries</source>\n        <target>{count, plural, =0 {No entry} one {One entry} other {# entries}}</target>\n      </trans-unit>\n      <trans-unit id=\"inLkptW\" resname=\"calendars.component.events\">\n        <source>calendars.component.events</source>\n        <target>Events</target>\n      </trans-unit>\n      <trans-unit id=\"m1s.bXE\" resname=\"calendars.component.todos\">\n        <source>calendars.component.todos</source>\n        <target>Todos</target>\n      </trans-unit>\n      <trans-unit id=\"Mx8PqQR\" resname=\"calendars.setup.title\">\n        <source>calendars.setup.title</source>\n        <target>Setup information</target>\n      </trans-unit>\n      <trans-unit id=\"jA5BLYc\" resname=\"calendars.component.notes\">\n        <source>calendars.component.notes</source>\n        <target>Notes</target>\n      </trans-unit>\n      <trans-unit id=\"O2IyoII\" resname=\"users.edit\">\n        <source>users.edit</source>\n        <target>Editing User «{username}»</target>\n      </trans-unit>\n      <trans-unit id=\"NPrwvQ_\" resname=\"addressbooks.for\">\n        <source>addressbooks.for</source>\n        <target>Address books for {who}</target>\n      </trans-unit>\n      <trans-unit id=\".ri3V4T\" resname=\"addressbooks.uri\">\n        <source>addressbooks.uri</source>\n        <target>Address book URI</target>\n      </trans-unit>\n      <trans-unit id=\"miq7_xK\" resname=\"addressbooks.contacts\">\n        <source>addressbooks.contacts</source>\n        <target>{count, plural, =0 {No contact} one {One contact} other {# contacts}}</target>\n      </trans-unit>\n      <trans-unit id=\"CqvStpa\" resname=\"calendars.for\">\n        <source>calendars.for</source>\n        <target>Calendars for {who}</target>\n      </trans-unit>\n      <trans-unit id=\"ybqHGGr\" resname=\"calendars.shared.with\">\n        <source>calendars.shared.with</source>\n        <target>Calendars shared with {who}</target>\n      </trans-unit>\n      <trans-unit id=\"caEhjpg\" resname=\"users.username\">\n        <source>users.username</source>\n        <target>Username</target>\n      </trans-unit>\n      <trans-unit id=\"kWHR7Qt\" resname=\"users.uri\">\n        <source>users.uri</source>\n        <target>Principal URI</target>\n      </trans-unit>\n      <trans-unit id=\"rD9wqzu\" resname=\"users.administrator\">\n        <source>users.administrator</source>\n        <target>Administrator</target>\n      </trans-unit>\n      <trans-unit id=\"tDUC2FT\" resname=\"users.calendars\">\n        <source>users.calendars</source>\n        <target>Calendars</target>\n      </trans-unit>\n      <trans-unit id=\"vrTk17r\" resname=\"users.addressbooks\">\n        <source>users.addressbooks</source>\n        <target>Address Books</target>\n      </trans-unit>\n      <trans-unit id=\"KB__Nh8\" resname=\"default.calendar.title\">\n        <source>default.calendar.title</source>\n        <target>Default Calendar</target>\n      </trans-unit>\n      <trans-unit id=\"ORuYu1p\" resname=\"default.calendar.description\">\n        <source>default.calendar.description</source>\n        <target>Default Calendar for {user}</target>\n      </trans-unit>\n      <trans-unit id=\"BV0O97e\" resname=\"default.addressbook.title\">\n        <source>default.addressbook.title</source>\n        <target>Default Address Book</target>\n      </trans-unit>\n      <trans-unit id=\"UKZXrbn\" resname=\"default.addressbook.description\">\n        <source>default.addressbook.description</source>\n        <target>Default Address Book for {user}</target>\n      </trans-unit>\n      <trans-unit id=\"4jEMVVj\" resname=\"user.saved\">\n        <source>user.saved</source>\n        <target>User saved successfully</target>\n      </trans-unit>\n      <trans-unit id=\"j1XlCAb\" resname=\"user.deleted\">\n        <source>user.deleted</source>\n        <target>User deleted successfully</target>\n      </trans-unit>\n      <trans-unit id=\"rey4Kec\" resname=\"calendar.saved\">\n        <source>calendar.saved</source>\n        <target>Calendar saved successfully</target>\n      </trans-unit>\n      <trans-unit id=\"UkuOpBs\" resname=\"calendar.deleted\">\n        <source>calendar.deleted</source>\n        <target>Calendar deleted successfully</target>\n      </trans-unit>\n      <trans-unit id=\"nLmfYXY\" resname=\"calendar.shared\">\n        <source>calendar.shared</source>\n        <target>Calendar shares modified successfully</target>\n      </trans-unit>\n      <trans-unit id=\"a7D0zuw\" resname=\"calendar.revoked\">\n        <source>calendar.revoked</source>\n        <target>Calendar revoked successfully</target>\n      </trans-unit>\n      <trans-unit id=\"pL5FhyS\" resname=\"calendars.delegates.member.add\">\n        <source>calendars.delegates.member.add</source>\n        <target>Share this calendar with another user:</target>\n      </trans-unit>\n      <trans-unit id=\"gdXa6Lc\" resname=\"calendars.delegates.member.help\">\n        <source>calendars.delegates.member.help</source>\n        <target>Adding a user who already has a shared access to this calendar will only affect its access right</target>\n      </trans-unit>\n      <trans-unit id=\"zWt8oG0\" resname=\"calendars.delegates.member.none\">\n        <source>calendars.delegates.member.none</source>\n        <target>There are no other user to share this calendar with</target>\n      </trans-unit>\n      <trans-unit id=\"ghe05Yl\" resname=\"addressbooks.saved\">\n        <source>addressbooks.saved</source>\n        <target>Address Book saved successfully</target>\n      </trans-unit>\n      <trans-unit id=\"SQckZXJ\" resname=\"addressbooks.deleted\">\n        <source>addressbooks.deleted</source>\n        <target>Address Book deleted successfully</target>\n      </trans-unit>\n      <trans-unit id=\"YcMyNwC\" resname=\"addressbooks.back\">\n        <source>addressbooks.back</source>\n        <target>Back to Address Books for {user}</target>\n      </trans-unit>\n      <trans-unit id=\"VgrgSjx\" resname=\"calendars.back\">\n        <source>calendars.back</source>\n        <target>Back to Calendars for {user}</target>\n      </trans-unit>\n      <trans-unit id=\"xNkdpFb\" resname=\"form.password.empty\">\n        <source>form.password.empty</source>\n        <target>Leave empty if you don't want to change it</target>\n      </trans-unit>\n      <trans-unit id=\"2wU1UVt\" resname=\"form.password.match\">\n        <source>form.password.match</source>\n        <target>The password fields must match.</target>\n      </trans-unit>\n      <trans-unit id=\"r2fjRrB\" resname=\"form.password\">\n        <source>form.password</source>\n        <target>Password</target>\n      </trans-unit>\n      <trans-unit id=\".EvZY.m\" resname=\"form.password.repeat\">\n        <source>form.password.repeat</source>\n        <target>Repeat Password</target>\n      </trans-unit>\n      <trans-unit id=\"vXLetA0\" resname=\"form.email\">\n        <source>form.email</source>\n        <target>Email</target>\n      </trans-unit>\n      <trans-unit id=\"RQWeqAY\" resname=\"form.admin\">\n        <source>form.admin</source>\n        <target>Is this user an administrator ?</target>\n      </trans-unit>\n      <trans-unit id=\"sLIuuSU\" resname=\"form.admin.help\">\n        <source>form.admin.help</source>\n        <target>If checked, this user (namely, its principal) will be injected in every single ACL rule with the '{DAV:}all' privileges.</target>\n      </trans-unit>\n      <trans-unit id=\"nXCRwB3\" resname=\"form.displayName\">\n        <source>form.displayName</source>\n        <target>Display name</target>\n      </trans-unit>\n      <trans-unit id=\"2_y0ttv\" resname=\"form.username\">\n        <source>form.username</source>\n        <target>Username</target>\n      </trans-unit>\n      <trans-unit id=\"iicAzGB\" resname=\"form.username.help\">\n        <source>form.username.help</source>\n        <target>May be an email, but not forcibly.</target>\n      </trans-unit>\n      <trans-unit id=\"IHOaweC\" resname=\"form.events\">\n        <source>form.events</source>\n        <target>Events</target>\n      </trans-unit>\n      <trans-unit id=\"4pbkJcC\" resname=\"form.notes\">\n        <source>form.notes</source>\n        <target>Notes</target>\n      </trans-unit>\n      <trans-unit id=\"dhJC0.u\" resname=\"form.todos\">\n        <source>form.todos</source>\n        <target>Todos</target>\n      </trans-unit>\n      <trans-unit id=\"ku2Vzal\" resname=\"form.uri\">\n        <source>form.uri</source>\n        <target>URI</target>\n      </trans-unit>\n      <trans-unit id=\"PHZs.1x\" resname=\"form.includedInBirthdayCalendar\">\n        <source>form.includedInBirthdayCalendar</source>\n        <target>Included in birthday calendar?</target>\n      </trans-unit>\n      <trans-unit id=\"GBcNXE.\" resname=\"form.description\">\n        <source>form.description</source>\n        <target>Description</target>\n      </trans-unit>\n      <trans-unit id=\"r_YZYfq\" resname=\"form.color\">\n        <source>form.color</source>\n        <target>Calendar color</target>\n      </trans-unit>\n      <trans-unit id=\"IPhqrmQ\" resname=\"form.public\">\n        <source>form.public</source>\n        <target>Public</target>\n      </trans-unit>\n      <trans-unit id=\"e02Cnv6\" resname=\"form.name.help.carddav\">\n        <source>form.name.help.carddav</source>\n        <target>This name will be displayed in your CardDAV client</target>\n      </trans-unit>\n      <trans-unit id=\"coqCyJL\" resname=\"form.includedInBirthdayCalendar.help\">\n        <source>form.includedInBirthdayCalendar.help</source>\n        <target>When selected, all cards with a valid birthday will be included in the principal's birthday calendar, which will be available as a shared calendar in your account</target>\n      </trans-unit>\n      <trans-unit id=\"rqpkW3X\" resname=\"form.name.help.caldav\">\n        <source>form.name.help.caldav</source>\n        <target>This name will be displayed in your CalDAV client</target>\n      </trans-unit>\n      <trans-unit id=\"XcmEaP5\" resname=\"form.uri.help.carddav\">\n        <source>form.uri.help.carddav</source>\n        <target>This is the unique identifier for this address book. Allowed characters are digits, lowercase letters and the dash symbol '-'.</target>\n      </trans-unit>\n      <trans-unit id=\"reeyEAg\" resname=\"form.uri.help.caldav\">\n        <source>form.uri.help.caldav</source>\n        <target>This is the unique identifier for this calendar. Allowed characters are digits, lowercase letters and the dash symbol '-'.</target>\n      </trans-unit>\n      <trans-unit id=\"xAoSnrW\" resname=\"form.public.help.caldav\">\n        <source>form.public.help.caldav</source>\n        <target>If the calendar is public, it will be available for anyone with the link</target>\n      </trans-unit>\n      <trans-unit id=\"OqHbbck\" resname=\"form.color.help\">\n        <source>form.color.help</source>\n        <target>This is the color that will be displayed in your CalDAV client. It must be supplied in the format '#RRGGBBAA' (alpha channel is optional) with hexadecimal values. This value is optional.</target>\n      </trans-unit>\n      <trans-unit id=\"ie6kT2V\" resname=\"form.events.help\">\n        <source>form.events.help</source>\n        <target>If checked, events will be enabled on this calendar. It will show up in calendar clients.</target>\n      </trans-unit>\n      <trans-unit id=\"m2d03h7\" resname=\"form.todos.help\">\n        <source>form.todos.help</source>\n        <target>If checked, todos will be enabled on this calendar. It will show up in reminders clients (such as macOS Reminders app)</target>\n      </trans-unit>\n      <trans-unit id=\"DZBm7go\" resname=\"form.notes.help\">\n        <source>form.notes.help</source>\n        <target>If checked, notes will be enabled on this calendar.</target>\n      </trans-unit>\n      <trans-unit id=\"nmAIJpy\" resname=\"addressbooks.modal.title\">\n        <source>addressbooks.modal.title</source>\n        <target>Delete this Address Book ?</target>\n      </trans-unit>\n      <trans-unit id=\"3mPrY9E\" resname=\"addressbooks.modal.text\">\n        <source>addressbooks.modal.text</source>\n        <target>Are you sure you want to delete this address book ? All the contacts in it will be deleted too.</target>\n      </trans-unit>\n      <trans-unit id=\"YmO7Wcu\" resname=\"calendars.modal.title\">\n        <source>calendars.modal.title</source>\n        <target>Delete this Calendar ?</target>\n      </trans-unit>\n      <trans-unit id=\"XM0TWcI\" resname=\"calendars.modal.text\">\n        <source>calendars.modal.text</source>\n        <target>Are you sure you want to delete this calendar ? All the events, todos and notes in it will be deleted too.</target>\n      </trans-unit>\n      <trans-unit id=\"3Own8oC\" resname=\"revoke.modal.title\">\n        <source>revoke.modal.title</source>\n        <target>Revoke access to this calendar ?</target>\n      </trans-unit>\n      <trans-unit id=\"N57f0XR\" resname=\"revoke.modal.text\">\n        <source>revoke.modal.text</source>\n        <target>Are you sure you want to revoke access to this calendar ? The user will lose access to all events, todos and notes. The original calendar will not be deleted.</target>\n      </trans-unit>\n      <trans-unit id=\"YVgPRJT\" resname=\"users.modal.title\">\n        <source>users.modal.title</source>\n        <target>Delete this User ?</target>\n      </trans-unit>\n      <trans-unit id=\"TBZP8bg\" resname=\"users.modal.text\">\n        <source>users.modal.text</source>\n        <target>Are you sure you want to delete this user ? All the associated calendars and address books will be deleted too.</target>\n      </trans-unit>\n      <trans-unit id=\"lyTSshw\" resname=\"delegates.modal.title\">\n        <source>delegates.modal.title</source>\n        <target>Remove this delegate ?</target>\n      </trans-unit>\n      <trans-unit id=\"MT5Ijud\" resname=\"delegates.modal.text\">\n        <source>delegates.modal.text</source>\n        <target>Are you sure you want to remove this delegate ? This user will no longer have access to the calendars, contacts, etc.</target>\n      </trans-unit>\n      <trans-unit id=\"0BNEzcg\" resname=\"delegates.member.help\">\n        <source>delegates.member.help</source>\n        <target>Adding a user who already is a delegate will only affect its access right</target>\n      </trans-unit>\n      <trans-unit id=\"WmbkJDd\" resname=\"cancel\">\n        <source>cancel</source>\n        <target>Cancel</target>\n      </trans-unit>\n      <trans-unit id=\"KoqKM6m\" resname=\"yes\">\n        <source>yes</source>\n        <target>Yes</target>\n      </trans-unit>\n      <trans-unit id=\"e3XmWtO\" resname=\"no\">\n        <source>no</source>\n        <target>No</target>\n      </trans-unit>\n      <trans-unit id=\"d7yLLjA\" resname=\"login.signin\">\n        <source>login.signin</source>\n        <target>Please sign in</target>\n      </trans-unit>\n      <trans-unit id=\"4TTdn8M\" resname=\"login.username\">\n        <source>login.username</source>\n        <target>Username</target>\n      </trans-unit>\n      <trans-unit id=\"sp0KUC6\" resname=\"login.password\">\n        <source>login.password</source>\n        <target>Password</target>\n      </trans-unit>\n      <trans-unit id=\"8jf9lcX\" resname=\"login.submit\">\n        <source>login.submit</source>\n        <target>Submit</target>\n      </trans-unit>\n      <trans-unit id=\"F55nsy3\" resname=\"logout\">\n        <source>logout</source>\n        <target>Logout</target>\n      </trans-unit>\n      <trans-unit id=\"GPvgh0L\" resname=\"login.already\">\n        <source>login.already</source>\n        <target>You are already logged in as {username}</target>\n      </trans-unit>\n      <trans-unit id=\"RcIhyKp\" resname=\"no.users.yet\">\n        <source>no.users.yet</source>\n        <target>No users yet.</target>\n      </trans-unit>\n      <trans-unit id=\"P5zs.4x\" resname=\"remove\">\n        <source>remove</source>\n        <target>Remove</target>\n      </trans-unit>\n      <trans-unit id=\"4HlJBp8\" resname=\"revoke\">\n        <source>revoke</source>\n        <target>Revoke</target>\n      </trans-unit>\n      <trans-unit id=\"SXq.3tZ\" resname=\"sharing\">\n        <source>sharing</source>\n        <target>Sharing</target>\n      </trans-unit>\n      <trans-unit id=\"elrbasT\" resname=\"calendars.sharing\">\n        <source>calendars.sharing</source>\n        <target>Calendar sharing</target>\n      </trans-unit>\n      <trans-unit id=\"HG_zUPj\" resname=\"calendars.delegates.add\">\n        <source>calendars.delegates.add</source>\n        <target>Add a delegate</target>\n      </trans-unit>\n      <trans-unit id=\"CIm3U7z\" resname=\"calendars.delegates.for\">\n        <source>calendars.delegates.for</source>\n        <target>Delegates for {what}</target>\n      </trans-unit>\n      <trans-unit id=\"0MBan0c\" resname=\"calendars.delegates.new\">\n        <source>calendars.delegates.new</source>\n        <target>New delegate</target>\n      </trans-unit>\n      <trans-unit id=\"P0vGXfx\" resname=\"calendars.delegates.existing\">\n        <source>calendars.delegates.existing</source>\n        <target>This calendar is shared with:</target>\n      </trans-unit>\n      <trans-unit id=\"0qEUvpG\" resname=\"calendars.delegates.none\">\n        <source>calendars.delegates.none</source>\n        <target>none</target>\n      </trans-unit>\n      <trans-unit id=\"p6swGey\" resname=\"calendars.delegates.member\">\n        <source>calendars.delegates.member</source>\n        <target>Member:</target>\n      </trans-unit>\n      <trans-unit id=\"sIayt9k\" resname=\"calendars.delegates.write.give\">\n        <source>calendars.delegates.write.give</source>\n        <target>Give write access ?</target>\n      </trans-unit>\n      <trans-unit id=\"oxkIwfV\" resname=\"users.delegates\">\n        <source>users.delegates</source>\n        <target>Delegates</target>\n      </trans-unit>\n      <trans-unit id=\"q.VJSGh\" resname=\"delegates.enabled.text\">\n        <source>delegates.enabled.text</source>\n        <target>Delegation is enabled for this account.</target>\n      </trans-unit>\n      <trans-unit id=\"42j2tRB\" resname=\"delegates.disable.warning\">\n        <source>delegates.disable.warning</source>\n        <target>⚠ If you disable it, all your delegates will lose access to this calendar</target>\n      </trans-unit>\n      <trans-unit id=\"HRSK3Wi\" resname=\"delegates.disable\">\n        <source>delegates.disable</source>\n        <target>Disable it</target>\n      </trans-unit>\n      <trans-unit id=\"KHtTcdj\" resname=\"delegates.disabled.text\">\n        <source>delegates.disabled.text</source>\n        <target>Delegation is not enabled for this account.</target>\n      </trans-unit>\n      <trans-unit id=\"bVvwuc_\" resname=\"delegates.enable\">\n        <source>delegates.enable</source>\n        <target>Enable it</target>\n      </trans-unit>\n      <trans-unit id=\"Au1oBii\" resname=\"delegates.write\">\n        <source>delegates.write</source>\n        <target>has write access</target>\n      </trans-unit>\n      <trans-unit id=\"R7lqMDG\" resname=\"delegates.readonly\">\n        <source>delegates.readonly</source>\n        <target>has readonly access</target>\n      </trans-unit>\n      <trans-unit id=\"TD5.lT1\" resname=\"calendar.share_access.2\">\n        <source>calendar.share_access.2</source>\n        <target>readonly</target>\n      </trans-unit>\n      <trans-unit id=\"wZOPHI8\" resname=\"calendar.share_access.3\">\n        <source>calendar.share_access.3</source>\n        <target>read / write</target>\n      </trans-unit>\n      <trans-unit id=\"F8h2YaT\" resname=\"calendar.public\">\n        <source>calendar.public</source>\n        <target>public</target>\n      </trans-unit>\n      <trans-unit id=\"mYDsetM\" resname=\"calendar.auto\">\n        <source>calendar.auto</source>\n        <target>auto</target>\n      </trans-unit>\n      <trans-unit id=\"FSDg5q6\" resname=\"calendar.subscription\">\n        <source>calendar.subscription</source>\n        <target>subscription</target>\n      </trans-unit>\n      <trans-unit id=\"mJ_hMaA\" resname=\"calendars.subscriptions\">\n        <source>calendars.subscriptions</source>\n        <target>Subscriptions</target>\n      </trans-unit>\n      <trans-unit id=\"ZnEWE47\" resname=\"calendars.auto\">\n        <source>calendars.auto</source>\n        <target>Automatically generated</target>\n      </trans-unit>\n    </body>\n  </file>\n</xliff>\n"
  },
  {
    "path": "translations/messages+intl-icu.fr.xliff",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file source-language=\"en\" target-language=\"fr\" datatype=\"plaintext\" original=\"file.ext\">\n    <header>\n      <tool tool-id=\"symfony\" tool-name=\"Symfony\"/>\n    </header>\n    <body>\n      <trans-unit id=\"poqANwU\" resname=\"davis\">\n        <source>davis</source>\n        <target>Davis</target>\n      </trans-unit>\n      <trans-unit id=\"IRk0vUS\" resname=\"enabled\">\n        <source>enabled</source>\n        <target>Activé</target>\n      </trans-unit>\n      <trans-unit id=\"iYiRxKz\" resname=\"disabled\">\n        <source>disabled</source>\n        <target>Désactivé</target>\n      </trans-unit>\n      <trans-unit id=\"04zDTq_\" resname=\"label.success\">\n        <source>label.success</source>\n        <target>Succès</target>\n      </trans-unit>\n      <trans-unit id=\"WcOat7C\" resname=\"label.warning\">\n        <source>label.warning</source>\n        <target>Avertissement</target>\n      </trans-unit>\n      <trans-unit id=\"v4zFdy0\" resname=\"label.error\">\n        <source>label.error</source>\n        <target>Erreur</target>\n      </trans-unit>\n      <trans-unit id=\"gyZcKbE\" resname=\"admin.interface\">\n        <source>admin.interface</source>\n        <target>Interface d'administration</target>\n      </trans-unit>\n      <trans-unit id=\"CAiOZVb\" resname=\"close\">\n        <source>close</source>\n        <target>Fermer</target>\n      </trans-unit>\n      <trans-unit id=\"2E3W_Ux\" resname=\"save\">\n        <source>save</source>\n        <target>Enregistrer</target>\n      </trans-unit>\n      <trans-unit id=\"4IcFUoR\" resname=\"toggle.navigation\">\n        <source>toggle.navigation</source>\n        <target>Basculer la navigation</target>\n      </trans-unit>\n      <trans-unit id=\"IY9tLQj\" resname=\"toggle.theme\">\n        <source>toggle.theme</source>\n        <target>Basculer le thème</target>\n      </trans-unit>\n      <trans-unit id=\"8z17OnR\" resname=\"theme.light\">\n        <source>theme.light</source>\n        <target>Clair</target>\n      </trans-unit>\n      <trans-unit id=\"5ZYx62e\" resname=\"theme.dark\">\n        <source>theme.dark</source>\n        <target>Sombre</target>\n      </trans-unit>\n      <trans-unit id=\"suizpO6\" resname=\"theme.auto\">\n        <source>theme.auto</source>\n        <target>Automatique</target>\n      </trans-unit>\n      <trans-unit id=\"YE7XVBb\" resname=\"title.dashboard\">\n        <source>title.dashboard</source>\n        <target>Tableau de bord</target>\n      </trans-unit>\n      <trans-unit id=\"G3gS3bJ\" resname=\"title.users_and_resources\">\n        <source>title.users_and_resources</source>\n        <target>Utilisateurs et ressources</target>\n      </trans-unit>\n      <trans-unit id=\"Ga1B1H3\" resname=\"users.back\">\n        <source>users.back</source>\n        <target>Retour aux utilisateurs</target>\n      </trans-unit>\n      <trans-unit id=\"2Mx633A\" resname=\"addressbooks.new\">\n        <source>addressbooks.new</source>\n        <target>Nouveau carnet d'adresses</target>\n      </trans-unit>\n      <trans-unit id=\"ddUyfGF\" resname=\"calendars.new\">\n        <source>calendars.new</source>\n        <target>Nouveau calendrier</target>\n      </trans-unit>\n      <trans-unit id=\"aCxiLZi\" resname=\"calendars.uri\">\n        <source>calendars.uri</source>\n        <target>URI</target>\n      </trans-unit>\n      <trans-unit id=\"JPI5wA3\" resname=\"users.new\">\n        <source>users.new</source>\n        <target>Nouvel utilisateur</target>\n      </trans-unit>\n      <trans-unit id=\"weOCfT2\" resname=\"dashboard.env\">\n        <source>dashboard.env</source>\n        <target>Environnement</target>\n      </trans-unit>\n      <trans-unit id=\"wx8fUKV\" resname=\"dashboard.capabilities\">\n        <source>dashboard.capabilities</source>\n        <target>Fonctionnalités</target>\n      </trans-unit>\n      <trans-unit id=\"zJSQAI8\" resname=\"dashboard.objects\">\n        <source>dashboard.objects</source>\n        <target>Objets</target>\n      </trans-unit>\n      <trans-unit id=\"mgKUnX3\" resname=\"dashboard.version\">\n        <source>dashboard.version</source>\n        <target>Version</target>\n      </trans-unit>\n      <trans-unit id=\"MpsZ_kY\" resname=\"dashboard.auth\">\n        <source>dashboard.auth</source>\n        <target>Authentification</target>\n      </trans-unit>\n      <trans-unit id=\"jNecvkm\" resname=\"dashboard.auth_realm\">\n        <source>dashboard.auth_realm</source>\n        <target>Domaine</target>\n      </trans-unit>\n      <trans-unit id=\"Utr1VRG\" resname=\"dashboard.invite_from_address\">\n        <source>dashboard.invite_from_address</source>\n        <target>Adresse d'invitation</target>\n      </trans-unit>\n      <trans-unit id=\"6A.HMvX\" resname=\"dashboard.server_timezone\">\n        <source>dashboard.server_timezone</source>\n        <target>Fuseau horaire de l'application (PHP)</target>\n      </trans-unit>\n      <trans-unit id=\"mD_kv2n\" resname=\"dashboard.bad_timezone_configuration\">\n        <source>dashboard.bad_timezone_configuration</source>\n        <target>Mauvaise configuration du fuseau horaire</target>\n      </trans-unit>\n      <trans-unit id=\"3jV45P0\" resname=\"dashboard.no_timezone_configuration\">\n        <source>dashboard.no_timezone_configuration</source>\n        <target>Non appliqué par l'application</target>\n      </trans-unit>\n      <trans-unit id=\"BDynkV_\" resname=\"dashboard.users\">\n        <source>dashboard.users</source>\n        <target>Utilisateurs</target>\n      </trans-unit>\n      <trans-unit id=\"oAGwPD5\" resname=\"dashboard.calendars\">\n        <source>dashboard.calendars</source>\n        <target>Calendriers</target>\n      </trans-unit>\n      <trans-unit id=\"79cIq7y\" resname=\"dashboard.calendars.help\">\n        <source>dashboard.calendars.help</source>\n        <target>(incl. listes de rappels)</target>\n      </trans-unit>\n      <trans-unit id=\"5SShPEM\" resname=\"dashboard.events\">\n        <source>dashboard.events</source>\n        <target>Événements</target>\n      </trans-unit>\n      <trans-unit id=\"kl9QvVx\" resname=\"dashboard.address_books\">\n        <source>dashboard.address_books</source>\n        <target>Carnets d'adresses</target>\n      </trans-unit>\n      <trans-unit id=\"OfamhPy\" resname=\"dashboard.contacts\">\n        <source>dashboard.contacts</source>\n        <target>Contacts</target>\n      </trans-unit>\n      <trans-unit id=\"iQm_wg0\" resname=\"addressbooks.edit\">\n        <source>addressbooks.edit</source>\n        <target>Modification du carnet d'adresses « {name} »</target>\n      </trans-unit>\n      <trans-unit id=\"BVAU8Tv\" resname=\"edit\">\n        <source>edit</source>\n        <target>Modifier</target>\n      </trans-unit>\n      <trans-unit id=\"JmmK.9X\" resname=\"add\">\n        <source>add</source>\n        <target>Ajouter</target>\n      </trans-unit>\n      <trans-unit id=\"u1x_1qs\" resname=\"delete\">\n        <source>delete</source>\n        <target>Supprimer</target>\n      </trans-unit>\n      <trans-unit id=\"lsvVz5G\" resname=\"calendars.edit\">\n        <source>calendars.edit</source>\n        <target>Modification du calendrier « {name} »</target>\n      </trans-unit>\n      <trans-unit id=\"1kNwhhb\" resname=\"calendars.entries\">\n        <source>calendars.entries</source>\n        <target>{count, plural, =0 {Aucune entrée} one {Une entrée} other {# entrées}}</target>\n      </trans-unit>\n      <trans-unit id=\"inLkptW\" resname=\"calendars.component.events\">\n        <source>calendars.component.events</source>\n        <target>Événements</target>\n      </trans-unit>\n      <trans-unit id=\"m1s.bXE\" resname=\"calendars.component.todos\">\n        <source>calendars.component.todos</source>\n        <target>Tâches</target>\n      </trans-unit>\n      <trans-unit id=\"Mx8PqQR\" resname=\"calendars.setup.title\">\n        <source>calendars.setup.title</source>\n        <target>Informations de configuration</target>\n      </trans-unit>\n      <trans-unit id=\"jA5BLYc\" resname=\"calendars.component.notes\">\n        <source>calendars.component.notes</source>\n        <target>Notes</target>\n      </trans-unit>\n      <trans-unit id=\"O2IyoII\" resname=\"users.edit\">\n        <source>users.edit</source>\n        <target>Modification de l'utilisateur « {username} »</target>\n      </trans-unit>\n      <trans-unit id=\"NPrwvQ_\" resname=\"addressbooks.for\">\n        <source>addressbooks.for</source>\n        <target>Carnets d'adresses de {who}</target>\n      </trans-unit>\n      <trans-unit id=\".ri3V4T\" resname=\"addressbooks.uri\">\n        <source>addressbooks.uri</source>\n        <target>URI du carnet d'adresses</target>\n      </trans-unit>\n      <trans-unit id=\"miq7_xK\" resname=\"addressbooks.contacts\">\n        <source>addressbooks.contacts</source>\n        <target>{count, plural, =0 {Aucun contact} one {Un contact} other {# contacts}}</target>\n      </trans-unit>\n      <trans-unit id=\"CqvStpa\" resname=\"calendars.for\">\n        <source>calendars.for</source>\n        <target>Calendriers de {who}</target>\n      </trans-unit>\n      <trans-unit id=\"ybqHGGr\" resname=\"calendars.shared.with\">\n        <source>calendars.shared.with</source>\n        <target>Calendriers partagés avec {who}</target>\n      </trans-unit>\n      <trans-unit id=\"caEhjpg\" resname=\"users.username\">\n        <source>users.username</source>\n        <target>Nom d'utilisateur</target>\n      </trans-unit>\n      <trans-unit id=\"kWHR7Qt\" resname=\"users.uri\">\n        <source>users.uri</source>\n        <target>URI principal</target>\n      </trans-unit>\n      <trans-unit id=\"rD9wqzu\" resname=\"users.administrator\">\n        <source>users.administrator</source>\n        <target>Administrateur</target>\n      </trans-unit>\n      <trans-unit id=\"tDUC2FT\" resname=\"users.calendars\">\n        <source>users.calendars</source>\n        <target>Calendriers</target>\n      </trans-unit>\n      <trans-unit id=\"vrTk17r\" resname=\"users.addressbooks\">\n        <source>users.addressbooks</source>\n        <target>Carnets d'adresses</target>\n      </trans-unit>\n      <trans-unit id=\"KB__Nh8\" resname=\"default.calendar.title\">\n        <source>default.calendar.title</source>\n        <target>Calendrier par défaut</target>\n      </trans-unit>\n      <trans-unit id=\"ORuYu1p\" resname=\"default.calendar.description\">\n        <source>default.calendar.description</source>\n        <target>Calendrier par défaut de {user}</target>\n      </trans-unit>\n      <trans-unit id=\"BV0O97e\" resname=\"default.addressbook.title\">\n        <source>default.addressbook.title</source>\n        <target>Carnet d'adresses par défaut</target>\n      </trans-unit>\n      <trans-unit id=\"UKZXrbn\" resname=\"default.addressbook.description\">\n        <source>default.addressbook.description</source>\n        <target>Carnet d'adresses par défaut de {user}</target>\n      </trans-unit>\n      <trans-unit id=\"4jEMVVj\" resname=\"user.saved\">\n        <source>user.saved</source>\n        <target>Utilisateur enregistré avec succès</target>\n      </trans-unit>\n      <trans-unit id=\"j1XlCAb\" resname=\"user.deleted\">\n        <source>user.deleted</source>\n        <target>Utilisateur supprimé avec succès</target>\n      </trans-unit>\n      <trans-unit id=\"rey4Kec\" resname=\"calendar.saved\">\n        <source>calendar.saved</source>\n        <target>Calendrier enregistré avec succès</target>\n      </trans-unit>\n      <trans-unit id=\"UkuOpBs\" resname=\"calendar.deleted\">\n        <source>calendar.deleted</source>\n        <target>Calendrier supprimé avec succès</target>\n      </trans-unit>\n      <trans-unit id=\"nLmfYXY\" resname=\"calendar.shared\">\n        <source>calendar.shared</source>\n        <target>Partages du calendrier modifiés avec succès</target>\n      </trans-unit>\n      <trans-unit id=\"a7D0zuw\" resname=\"calendar.revoked\">\n        <source>calendar.revoked</source>\n        <target>Accès au calendrier révoqué avec succès</target>\n      </trans-unit>\n      <trans-unit id=\"pL5FhyS\" resname=\"calendars.delegates.member.add\">\n        <source>calendars.delegates.member.add</source>\n        <target>Partager ce calendrier avec un autre utilisateur :</target>\n      </trans-unit>\n      <trans-unit id=\"gdXa6Lc\" resname=\"calendars.delegates.member.help\">\n        <source>calendars.delegates.member.help</source>\n        <target>L'ajout d'un utilisateur ayant déjà un accès partagé à ce calendrier n'affectera que ses droits d'accès</target>\n      </trans-unit>\n      <trans-unit id=\"zWt8oG0\" resname=\"calendars.delegates.member.none\">\n        <source>calendars.delegates.member.none</source>\n        <target>Il n'y a aucun autre utilisateur avec lequel partager ce calendrier</target>\n      </trans-unit>\n      <trans-unit id=\"ghe05Yl\" resname=\"addressbooks.saved\">\n        <source>addressbooks.saved</source>\n        <target>Carnet d'adresses enregistré avec succès</target>\n      </trans-unit>\n      <trans-unit id=\"SQckZXJ\" resname=\"addressbooks.deleted\">\n        <source>addressbooks.deleted</source>\n        <target>Carnet d'adresses supprimé avec succès</target>\n      </trans-unit>\n      <trans-unit id=\"YcMyNwC\" resname=\"addressbooks.back\">\n        <source>addressbooks.back</source>\n        <target>Retour aux carnets d'adresses de {user}</target>\n      </trans-unit>\n      <trans-unit id=\"VgrgSjx\" resname=\"calendars.back\">\n        <source>calendars.back</source>\n        <target>Retour aux calendriers de {user}</target>\n      </trans-unit>\n      <trans-unit id=\"xNkdpFb\" resname=\"form.password.empty\">\n        <source>form.password.empty</source>\n        <target>Laissez vide si vous ne souhaitez pas le modifier</target>\n      </trans-unit>\n      <trans-unit id=\"2wU1UVt\" resname=\"form.password.match\">\n        <source>form.password.match</source>\n        <target>Les champs de mot de passe doivent correspondre.</target>\n      </trans-unit>\n      <trans-unit id=\"r2fjRrB\" resname=\"form.password\">\n        <source>form.password</source>\n        <target>Mot de passe</target>\n      </trans-unit>\n      <trans-unit id=\".EvZY.m\" resname=\"form.password.repeat\">\n        <source>form.password.repeat</source>\n        <target>Répéter le mot de passe</target>\n      </trans-unit>\n      <trans-unit id=\"vXLetA0\" resname=\"form.email\">\n        <source>form.email</source>\n        <target>Email</target>\n      </trans-unit>\n      <trans-unit id=\"RQWeqAY\" resname=\"form.admin\">\n        <source>form.admin</source>\n        <target>Cet utilisateur est-il administrateur ?</target>\n      </trans-unit>\n      <trans-unit id=\"sLIuuSU\" resname=\"form.admin.help\">\n        <source>form.admin.help</source>\n        <target>Si coché, cet utilisateur (notamment son principal) sera injecté dans chaque règle ACL avec les privilèges '{DAV:}all'</target>\n      </trans-unit>\n      <trans-unit id=\"nXCRwB3\" resname=\"form.displayName\">\n        <source>form.displayName</source>\n        <target>Nom d'affichage</target>\n      </trans-unit>\n      <trans-unit id=\"2_y0ttv\" resname=\"form.username\">\n        <source>form.username</source>\n        <target>Nom d'utilisateur</target>\n      </trans-unit>\n      <trans-unit id=\"iicAzGB\" resname=\"form.username.help\">\n        <source>form.username.help</source>\n        <target>Peut être un email, mais pas obligatoirement.</target>\n      </trans-unit>\n      <trans-unit id=\"IHOaweC\" resname=\"form.events\">\n        <source>form.events</source>\n        <target>Événements</target>\n      </trans-unit>\n      <trans-unit id=\"4pbkJcC\" resname=\"form.notes\">\n        <source>form.notes</source>\n        <target>Notes</target>\n      </trans-unit>\n      <trans-unit id=\"dhJC0.u\" resname=\"form.todos\">\n        <source>form.todos</source>\n        <target>Tâches</target>\n      </trans-unit>\n      <trans-unit id=\"ku2Vzal\" resname=\"form.uri\">\n        <source>form.uri</source>\n        <target>URI</target>\n      </trans-unit>\n      <trans-unit id=\"PHZs.1x\" resname=\"form.includedInBirthdayCalendar\">\n        <source>form.includedInBirthdayCalendar</source>\n        <target>Inclus dans le calendrier des anniversaires ?</target>\n      </trans-unit>\n      <trans-unit id=\"GBcNXE.\" resname=\"form.description\">\n        <source>form.description</source>\n        <target>Description</target>\n      </trans-unit>\n      <trans-unit id=\"r_YZYfq\" resname=\"form.color\">\n        <source>form.color</source>\n        <target>Couleur du calendrier</target>\n      </trans-unit>\n      <trans-unit id=\"IPhqrmQ\" resname=\"form.public\">\n        <source>form.public</source>\n        <target>Public</target>\n      </trans-unit>\n      <trans-unit id=\"e02Cnv6\" resname=\"form.name.help.carddav\">\n        <source>form.name.help.carddav</source>\n        <target>Ce nom s'affichera dans votre client CardDAV</target>\n      </trans-unit>\n      <trans-unit id=\"coqCyJL\" resname=\"form.includedInBirthdayCalendar.help\">\n        <source>form.includedInBirthdayCalendar.help</source>\n        <target>Lorsque cette option est sélectionnée, toutes les cartes contenant un anniversaire valide seront incluses dans le calendrier des anniversaires du compte principal, disponible comme calendrier partagé dans votre compte</target>\n      </trans-unit>\n      <trans-unit id=\"rqpkW3X\" resname=\"form.name.help.caldav\">\n        <source>form.name.help.caldav</source>\n        <target>Ce nom s'affichera dans votre client CalDAV</target>\n      </trans-unit>\n      <trans-unit id=\"XcmEaP5\" resname=\"form.uri.help.carddav\">\n        <source>form.uri.help.carddav</source>\n        <target>C'est l'identifiant unique pour ce carnet d'adresses. Les caractères autorisés sont les chiffres, les lettres minuscules et le tiret « - ».</target>\n      </trans-unit>\n      <trans-unit id=\"reeyEAg\" resname=\"form.uri.help.caldav\">\n        <source>form.uri.help.caldav</source>\n        <target>C'est l'identifiant unique pour ce calendrier. Les caractères autorisés sont les chiffres, les lettres minuscules et le tiret « - ».</target>\n      </trans-unit>\n      <trans-unit id=\"xAoSnrW\" resname=\"form.public.help.caldav\">\n        <source>form.public.help.caldav</source>\n        <target>Si le calendrier est public, il sera disponible pour toute personne ayant le lien</target>\n      </trans-unit>\n      <trans-unit id=\"OqHbbck\" resname=\"form.color.help\">\n        <source>form.color.help</source>\n        <target>C'est la couleur qui s'affichera dans votre client CalDAV. Elle doit être fournie au format « #RRGGBBAA » (le canal alpha est optionnel) avec des valeurs hexadécimales. Cette valeur est optionnelle.</target>\n      </trans-unit>\n      <trans-unit id=\"ie6kT2V\" resname=\"form.events.help\">\n        <source>form.events.help</source>\n        <target>Si coché, les événements seront activés sur ce calendrier. Il apparaîtra dans les clients calendrier.</target>\n      </trans-unit>\n      <trans-unit id=\"m2d03h7\" resname=\"form.todos.help\">\n        <source>form.todos.help</source>\n        <target>Si coché, les tâches seront activées sur ce calendrier. Il apparaîtra dans les clients de rappels (comme l'app Rappels de macOS)</target>\n      </trans-unit>\n      <trans-unit id=\"DZBm7go\" resname=\"form.notes.help\">\n        <source>form.notes.help</source>\n        <target>Si coché, les notes seront activées sur ce calendrier.</target>\n      </trans-unit>\n      <trans-unit id=\"nmAIJpy\" resname=\"addressbooks.modal.title\">\n        <source>addressbooks.modal.title</source>\n        <target>Supprimer ce carnet d'adresses ?</target>\n      </trans-unit>\n      <trans-unit id=\"3mPrY9E\" resname=\"addressbooks.modal.text\">\n        <source>addressbooks.modal.text</source>\n        <target>Êtes-vous sûr de vouloir supprimer ce carnet d'adresses ? Tous les contacts qu'il contient seront supprimés.</target>\n      </trans-unit>\n      <trans-unit id=\"YmO7Wcu\" resname=\"calendars.modal.title\">\n        <source>calendars.modal.title</source>\n        <target>Supprimer ce calendrier ?</target>\n      </trans-unit>\n      <trans-unit id=\"XM0TWcI\" resname=\"calendars.modal.text\">\n        <source>calendars.modal.text</source>\n        <target>Êtes-vous sûr de vouloir supprimer ce calendrier ? Tous les événements, tâches et notes qu'il contient seront supprimés.</target>\n      </trans-unit>\n      <trans-unit id=\"3Own8oC\" resname=\"revoke.modal.title\">\n        <source>revoke.modal.title</source>\n        <target>Révoquer l'accès à ce calendrier ?</target>\n      </trans-unit>\n      <trans-unit id=\"N57f0XR\" resname=\"revoke.modal.text\">\n        <source>revoke.modal.text</source>\n        <target>Êtes-vous sûr de vouloir révoquer l'accès à ce calendrier ? L'utilisateur perdra l'accès à tous les événements, tâches et notes. Le calendrier d'origine ne sera pas supprimé.</target>\n      </trans-unit>\n      <trans-unit id=\"YVgPRJT\" resname=\"users.modal.title\">\n        <source>users.modal.title</source>\n        <target>Supprimer cet utilisateur ?</target>\n      </trans-unit>\n      <trans-unit id=\"TBZP8bg\" resname=\"users.modal.text\">\n        <source>users.modal.text</source>\n        <target>Êtes-vous sûr de vouloir supprimer cet utilisateur ? Tous les calendriers et carnets d'adresses associés seront supprimés.</target>\n      </trans-unit>\n      <trans-unit id=\"lyTSshw\" resname=\"delegates.modal.title\">\n        <source>delegates.modal.title</source>\n        <target>Retirer ce délégué ?</target>\n      </trans-unit>\n      <trans-unit id=\"MT5Ijud\" resname=\"delegates.modal.text\">\n        <source>delegates.modal.text</source>\n        <target>Êtes-vous sûr de vouloir retirer ce délégué ? Cet utilisateur n'aura plus accès aux calendriers, contacts, etc.</target>\n      </trans-unit>\n      <trans-unit id=\"0BNEzcg\" resname=\"delegates.member.help\">\n        <source>delegates.member.help</source>\n        <target>L'ajout d'un utilisateur qui est déjà délégué n'affectera que ses droits d'accès</target>\n      </trans-unit>\n      <trans-unit id=\"WmbkJDd\" resname=\"cancel\">\n        <source>cancel</source>\n        <target>Annuler</target>\n      </trans-unit>\n      <trans-unit id=\"KoqKM6m\" resname=\"yes\">\n        <source>yes</source>\n        <target>Oui</target>\n      </trans-unit>\n      <trans-unit id=\"e3XmWtO\" resname=\"no\">\n        <source>no</source>\n        <target>Non</target>\n      </trans-unit>\n      <trans-unit id=\"d7yLLjA\" resname=\"login.signin\">\n        <source>login.signin</source>\n        <target>Veuillez vous connecter</target>\n      </trans-unit>\n      <trans-unit id=\"4TTdn8M\" resname=\"login.username\">\n        <source>login.username</source>\n        <target>Nom d'utilisateur</target>\n      </trans-unit>\n      <trans-unit id=\"sp0KUC6\" resname=\"login.password\">\n        <source>login.password</source>\n        <target>Mot de passe</target>\n      </trans-unit>\n      <trans-unit id=\"8jf9lcX\" resname=\"login.submit\">\n        <source>login.submit</source>\n        <target>Soumettre</target>\n      </trans-unit>\n      <trans-unit id=\"F55nsy3\" resname=\"logout\">\n        <source>logout</source>\n        <target>Se déconnecter</target>\n      </trans-unit>\n      <trans-unit id=\"GPvgh0L\" resname=\"login.already\">\n        <source>login.already</source>\n        <target>Vous êtes déjà connecté en tant que {username}</target>\n      </trans-unit>\n      <trans-unit id=\"RcIhyKp\" resname=\"no.users.yet\">\n        <source>no.users.yet</source>\n        <target>Aucun utilisateur pour l'instant.</target>\n      </trans-unit>\n      <trans-unit id=\"P5zs.4x\" resname=\"remove\">\n        <source>remove</source>\n        <target>Retirer</target>\n      </trans-unit>\n      <trans-unit id=\"4HlJBp8\" resname=\"revoke\">\n        <source>revoke</source>\n        <target>Révoquer</target>\n      </trans-unit>\n      <trans-unit id=\"SXq.3tZ\" resname=\"sharing\">\n        <source>sharing</source>\n        <target>Partage</target>\n      </trans-unit>\n      <trans-unit id=\"elrbasT\" resname=\"calendars.sharing\">\n        <source>calendars.sharing</source>\n        <target>Partage du calendrier</target>\n      </trans-unit>\n      <trans-unit id=\"HG_zUPj\" resname=\"calendars.delegates.add\">\n        <source>calendars.delegates.add</source>\n        <target>Ajouter un délégué</target>\n      </trans-unit>\n      <trans-unit id=\"CIm3U7z\" resname=\"calendars.delegates.for\">\n        <source>calendars.delegates.for</source>\n        <target>Délégués de {what}</target>\n      </trans-unit>\n      <trans-unit id=\"0MBan0c\" resname=\"calendars.delegates.new\">\n        <source>calendars.delegates.new</source>\n        <target>Nouveau délégué</target>\n      </trans-unit>\n      <trans-unit id=\"P0vGXfx\" resname=\"calendars.delegates.existing\">\n        <source>calendars.delegates.existing</source>\n        <target>Ce calendrier est partagé avec :</target>\n      </trans-unit>\n      <trans-unit id=\"0qEUvpG\" resname=\"calendars.delegates.none\">\n        <source>calendars.delegates.none</source>\n        <target>aucun</target>\n      </trans-unit>\n      <trans-unit id=\"p6swGey\" resname=\"calendars.delegates.member\">\n        <source>calendars.delegates.member</source>\n        <target>Membre :</target>\n      </trans-unit>\n      <trans-unit id=\"sIayt9k\" resname=\"calendars.delegates.write.give\">\n        <source>calendars.delegates.write.give</source>\n        <target>Accorder l'accès en écriture ?</target>\n      </trans-unit>\n      <trans-unit id=\"oxkIwfV\" resname=\"users.delegates\">\n        <source>users.delegates</source>\n        <target>Délégués</target>\n      </trans-unit>\n      <trans-unit id=\"q.VJSGh\" resname=\"delegates.enabled.text\">\n        <source>delegates.enabled.text</source>\n        <target>La délégation est activée pour ce compte.</target>\n      </trans-unit>\n      <trans-unit id=\"42j2tRB\" resname=\"delegates.disable.warning\">\n        <source>delegates.disable.warning</source>\n        <target>⚠ Si vous la désactivez, tous vos délégués perdront l'accès à ce calendrier</target>\n      </trans-unit>\n      <trans-unit id=\"HRSK3Wi\" resname=\"delegates.disable\">\n        <source>delegates.disable</source>\n        <target>La désactiver</target>\n      </trans-unit>\n      <trans-unit id=\"KHtTcdj\" resname=\"delegates.disabled.text\">\n        <source>delegates.disabled.text</source>\n        <target>La délégation n'est pas activée pour ce compte.</target>\n      </trans-unit>\n      <trans-unit id=\"bVvwuc_\" resname=\"delegates.enable\">\n        <source>delegates.enable</source>\n        <target>L'activer</target>\n      </trans-unit>\n      <trans-unit id=\"Au1oBii\" resname=\"delegates.write\">\n        <source>delegates.write</source>\n        <target>a l'accès en écriture</target>\n      </trans-unit>\n      <trans-unit id=\"R7lqMDG\" resname=\"delegates.readonly\">\n        <source>delegates.readonly</source>\n        <target>a l'accès en lecture seule</target>\n      </trans-unit>\n      <trans-unit id=\"TD5.lT1\" resname=\"calendar.share_access.2\">\n        <source>calendar.share_access.2</source>\n        <target>lecture seule</target>\n      </trans-unit>\n      <trans-unit id=\"wZOPHI8\" resname=\"calendar.share_access.3\">\n        <source>calendar.share_access.3</source>\n        <target>lecture / écriture</target>\n      </trans-unit>\n      <trans-unit id=\"F8h2YaT\" resname=\"calendar.share_access.10\">\n        <source>calendar.share_access.10</source>\n        <target>public</target>\n      </trans-unit>\n      <trans-unit id=\"mYDsetM\" resname=\"calendar.auto\">\n        <source>calendar.auto</source>\n        <target>automatique</target>\n      </trans-unit>\n      <trans-unit id=\"FSDg5q6\" resname=\"calendar.subscription\">\n        <source>calendar.subscription</source>\n        <target>abonnement</target>\n      </trans-unit>\n      <trans-unit id=\"mJ_hMaA\" resname=\"calendars.subscriptions\">\n        <source>calendars.subscriptions</source>\n        <target>Abonnements</target>\n      </trans-unit>\n      <trans-unit id=\"ZnEWE47\" resname=\"calendars.auto\">\n        <source>calendars.auto</source>\n        <target>Générés automatiquement</target>\n      </trans-unit>\n    </body>\n  </file>\n</xliff>\n"
  },
  {
    "path": "translations/security.de.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file source-language=\"en\" target-language=\"de\" datatype=\"plaintext\" original=\"file.ext\">\n    <header>\n      <tool tool-id=\"symfony\" tool-name=\"Symfony\"/>\n    </header>\n    <body>\n      <trans-unit id=\"baI_ZxO\" resname=\"An authentication exception occurred.\">\n        <source>An authentication exception occurred.</source>\n        <target>Es ist eine Authentifizierungsausnahme aufgetreten.</target>\n      </trans-unit>\n      <trans-unit id=\"OETylMq\" resname=\"Authentication credentials could not be found.\">\n        <source>Authentication credentials could not be found.</source>\n        <target>Die Authentifizierungsdaten konnten nicht gefunden werden.</target>\n      </trans-unit>\n      <trans-unit id=\"3RJINQ0\" resname=\"Authentication request could not be processed due to a system problem.\">\n        <source>Authentication request could not be processed due to a system problem.</source>\n        <target>Die Authentifizierungsanfrage konnte aufgrund eines Systemproblems nicht verarbeitet werden.</target>\n      </trans-unit>\n      <trans-unit id=\"qr0aiUo\" resname=\"Invalid credentials.\">\n        <source>Invalid credentials.</source>\n        <target>Ungültige Zugangsdaten.</target>\n      </trans-unit>\n      <trans-unit id=\"zrJWK0F\" resname=\"Cookie has already been used by someone else.\">\n        <source>Cookie has already been used by someone else.</source>\n        <target>Der Cookie wurde bereits von jemand anderem verwendet.</target>\n      </trans-unit>\n      <trans-unit id=\"blC0fXX\" resname=\"Not privileged to request the resource.\">\n        <source>Not privileged to request the resource.</source>\n        <target>Nicht berechtigt die Ressource anzufordern.</target>\n      </trans-unit>\n      <trans-unit id=\"dLzMRPR\" resname=\"Invalid CSRF token.\">\n        <source>Invalid CSRF token.</source>\n        <target>Ungültiger CSRF-Token.</target>\n      </trans-unit>\n      <trans-unit id=\"PhhlLem\" resname=\"No authentication provider found to support the authentication token.\">\n        <source>No authentication provider found to support the authentication token.</source>\n        <target>Es wurde kein Authentifizierungsanbieter gefunden, der den Authentifizierungstoken unterstützt.</target>\n      </trans-unit>\n      <trans-unit id=\"v_RS21A\" resname=\"No session available, it either timed out or cookies are not enabled.\">\n        <source>No session available, it either timed out or cookies are not enabled.</source>\n        <target>Keine Sitzung verfügbar, entweder ist die Zeit abgelaufen oder die Cookies sind nicht aktiviert.</target>\n      </trans-unit>\n      <trans-unit id=\"EYCKpDH\" resname=\"No token could be found.\">\n        <source>No token could be found.</source>\n        <target>Es wurde kein Token gefunden.</target>\n      </trans-unit>\n      <trans-unit id=\"z3cOUZo\" resname=\"Username could not be found.\">\n        <source>Username could not be found.</source>\n        <target>Ungültige Zugangsdaten.</target>\n      </trans-unit>\n      <trans-unit id=\"By5eLYM\" resname=\"Account has expired.\">\n        <source>Account has expired.</source>\n        <target>Das Konto ist abgelaufen.</target>\n      </trans-unit>\n      <trans-unit id=\"YfZhiuA\" resname=\"Credentials have expired.\">\n        <source>Credentials have expired.</source>\n        <target>Die Zugangsdaten sind abgelaufen.</target>\n      </trans-unit>\n      <trans-unit id=\"NrSSfLs\" resname=\"Account is disabled.\">\n        <source>Account is disabled.</source>\n        <target>Das Konto ist deaktiviert.</target>\n      </trans-unit>\n      <trans-unit id=\"O5ZyxHr\" resname=\"Account is locked.\">\n        <source>Account is locked.</source>\n        <target>Das Konto ist gesperrt.</target>\n      </trans-unit>\n    </body>\n  </file>\n</xliff>\n"
  },
  {
    "path": "translations/security.en.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file source-language=\"en\" target-language=\"en\" datatype=\"plaintext\" original=\"file.ext\">\n    <header>\n      <tool tool-id=\"symfony\" tool-name=\"Symfony\"/>\n    </header>\n    <body>\n      <trans-unit id=\"baI_ZxO\" resname=\"An authentication exception occurred.\">\n        <source>An authentication exception occurred.</source>\n        <target>An authentication exception occurred.</target>\n      </trans-unit>\n      <trans-unit id=\"OETylMq\" resname=\"Authentication credentials could not be found.\">\n        <source>Authentication credentials could not be found.</source>\n        <target>Authentication credentials could not be found.</target>\n      </trans-unit>\n      <trans-unit id=\"3RJINQ0\" resname=\"Authentication request could not be processed due to a system problem.\">\n        <source>Authentication request could not be processed due to a system problem.</source>\n        <target>Authentication request could not be processed due to a system problem.</target>\n      </trans-unit>\n      <trans-unit id=\"qr0aiUo\" resname=\"Invalid credentials.\">\n        <source>Invalid credentials.</source>\n        <target>Invalid credentials.</target>\n      </trans-unit>\n      <trans-unit id=\"zrJWK0F\" resname=\"Cookie has already been used by someone else.\">\n        <source>Cookie has already been used by someone else.</source>\n        <target>Cookie has already been used by someone else.</target>\n      </trans-unit>\n      <trans-unit id=\"blC0fXX\" resname=\"Not privileged to request the resource.\">\n        <source>Not privileged to request the resource.</source>\n        <target>Not privileged to request the resource.</target>\n      </trans-unit>\n      <trans-unit id=\"dLzMRPR\" resname=\"Invalid CSRF token.\">\n        <source>Invalid CSRF token.</source>\n        <target>Invalid CSRF token.</target>\n      </trans-unit>\n      <trans-unit id=\"PhhlLem\" resname=\"No authentication provider found to support the authentication token.\">\n        <source>No authentication provider found to support the authentication token.</source>\n        <target>No authentication provider found to support the authentication token.</target>\n      </trans-unit>\n      <trans-unit id=\"v_RS21A\" resname=\"No session available, it either timed out or cookies are not enabled.\">\n        <source>No session available, it either timed out or cookies are not enabled.</source>\n        <target>No session available, it either timed out or cookies are not enabled.</target>\n      </trans-unit>\n      <trans-unit id=\"EYCKpDH\" resname=\"No token could be found.\">\n        <source>No token could be found.</source>\n        <target>No token could be found.</target>\n      </trans-unit>\n      <trans-unit id=\"z3cOUZo\" resname=\"Username could not be found.\">\n        <source>Username could not be found.</source>\n        <target>Invalid credentials.</target>\n      </trans-unit>\n      <trans-unit id=\"By5eLYM\" resname=\"Account has expired.\">\n        <source>Account has expired.</source>\n        <target>Account has expired.</target>\n      </trans-unit>\n      <trans-unit id=\"YfZhiuA\" resname=\"Credentials have expired.\">\n        <source>Credentials have expired.</source>\n        <target>Credentials have expired.</target>\n      </trans-unit>\n      <trans-unit id=\"NrSSfLs\" resname=\"Account is disabled.\">\n        <source>Account is disabled.</source>\n        <target>Account is disabled.</target>\n      </trans-unit>\n      <trans-unit id=\"O5ZyxHr\" resname=\"Account is locked.\">\n        <source>Account is locked.</source>\n        <target>Account is locked.</target>\n      </trans-unit>\n    </body>\n  </file>\n</xliff>\n"
  },
  {
    "path": "translations/security.fr.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n    <file source-language=\"en\" target-language=\"fr\" datatype=\"plaintext\" original=\"file.ext\">\n        <body>\n            <trans-unit id=\"1\">\n                <source>An authentication exception occurred.</source>\n                <target>Une exception d'authentification s'est produite.</target>\n            </trans-unit>\n            <trans-unit id=\"2\">\n                <source>Authentication credentials could not be found.</source>\n                <target>Les identifiants d'authentification n'ont pas pu être trouvés.</target>\n            </trans-unit>\n            <trans-unit id=\"3\">\n                <source>Authentication request could not be processed due to a system problem.</source>\n                <target>La requête d'authentification n'a pas pu être executée à cause d'un problème système.</target>\n            </trans-unit>\n            <trans-unit id=\"4\">\n                <source>Invalid credentials.</source>\n                <target>Identifiants invalides.</target>\n            </trans-unit>\n            <trans-unit id=\"5\">\n                <source>Cookie has already been used by someone else.</source>\n                <target>Le cookie a déjà été utilisé par quelqu'un d'autre.</target>\n            </trans-unit>\n            <trans-unit id=\"6\">\n                <source>Not privileged to request the resource.</source>\n                <target>Privilèges insuffisants pour accéder à la ressource.</target>\n            </trans-unit>\n            <trans-unit id=\"7\">\n                <source>Invalid CSRF token.</source>\n                <target>Jeton CSRF invalide.</target>\n            </trans-unit>\n            <trans-unit id=\"9\">\n                <source>No authentication provider found to support the authentication token.</source>\n                <target>Aucun fournisseur d'authentification n'a été trouvé pour supporter le jeton d'authentification.</target>\n            </trans-unit>\n            <trans-unit id=\"10\">\n                <source>No session available, it either timed out or cookies are not enabled.</source>\n                <target>Aucune session disponible, celle-ci a expiré ou les cookies ne sont pas activés.</target>\n            </trans-unit>\n            <trans-unit id=\"11\">\n                <source>No token could be found.</source>\n                <target>Aucun jeton n'a pu être trouvé.</target>\n            </trans-unit>\n            <trans-unit id=\"12\">\n                <source>Username could not be found.</source>\n                <target>Identifiants invalides.</target>\n            </trans-unit>\n            <trans-unit id=\"13\">\n                <source>Account has expired.</source>\n                <target>Le compte a expiré.</target>\n            </trans-unit>\n            <trans-unit id=\"14\">\n                <source>Credentials have expired.</source>\n                <target>Les identifiants ont expiré.</target>\n            </trans-unit>\n            <trans-unit id=\"15\">\n                <source>Account is disabled.</source>\n                <target>Le compte est désactivé.</target>\n            </trans-unit>\n            <trans-unit id=\"16\">\n                <source>Account is locked.</source>\n                <target>Le compte est bloqué.</target>\n            </trans-unit>\n            <trans-unit id=\"17\">\n                <source>Too many failed login attempts, please try again later.</source>\n                <target>Plusieurs tentatives de connexion ont échoué, veuillez réessayer plus tard.</target>\n            </trans-unit>\n            <trans-unit id=\"18\">\n                <source>Invalid or expired login link.</source>\n                <target>Lien de connexion invalide ou expiré.</target>\n            </trans-unit>\n            <trans-unit id=\"19\">\n                <source>Too many failed login attempts, please try again in %minutes% minute.</source>\n                <target>Plusieurs tentatives de connexion ont échoué, veuillez réessayer dans %minutes% minute.</target>\n            </trans-unit>\n            <trans-unit id=\"20\">\n                <source>Too many failed login attempts, please try again in %minutes% minutes.</source>\n                <target>Trop de tentatives de connexion échouées, veuillez réessayer dans %minutes% minutes.</target>\n            </trans-unit>\n        </body>\n    </file>\n</xliff>"
  },
  {
    "path": "translations/validators.de.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file source-language=\"en\" target-language=\"de\" datatype=\"plaintext\" original=\"file.ext\">\n    <header>\n      <tool tool-id=\"symfony\" tool-name=\"Symfony\"/>\n    </header>\n    <body>\n      <trans-unit id=\"zh5oKD9\" resname=\"This value should be false.\">\n        <source>This value should be false.</source>\n        <target>Dieser Wert solle \"false\" sein.</target>\n      </trans-unit>\n      <trans-unit id=\"NN2_iru\" resname=\"This value should be true.\">\n        <source>This value should be true.</source>\n        <target>Dieser Wert sollte \"true\" sein.</target>\n      </trans-unit>\n      <trans-unit id=\"OjM_kpf\" resname=\"This value should be of type {{ type }}.\">\n        <source>This value should be of type {{ type }}.</source>\n        <target>Dieser Wert sollte vom Typ {{ type }} sein.</target>\n      </trans-unit>\n      <trans-unit id=\"f0P5pBD\" resname=\"This value should be blank.\">\n        <source>This value should be blank.</source>\n        <target>Dieser Wert sollte leer sein.</target>\n      </trans-unit>\n      <trans-unit id=\"ih.4TRN\" resname=\"The value you selected is not a valid choice.\">\n        <source>The value you selected is not a valid choice.</source>\n        <target>Der von Ihnen gewählte Wert ist keine gültige Auswahl.</target>\n      </trans-unit>\n      <trans-unit id=\"u81CkP8\" resname=\"You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.\">\n        <source>You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.</source>\n        <target>Sie müssen mindestens {{ Limit }} Auswahlmöglichkeiten wählen.|Sie müssen mindestens {{ Limit }} Auswahlmöglichkeiten wählen.</target>\n      </trans-unit>\n      <trans-unit id=\"nIvOQ_o\" resname=\"You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.\">\n        <source>You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.</source>\n        <target>Sie dürfen höchstens {{ Limit }} Auswahlmöglichkeiten wählen.|Sie dürfen höchstens {{ Limit }} Auswahlmöglichkeiten wählen.</target>\n      </trans-unit>\n      <trans-unit id=\"87zjiHi\" resname=\"One or more of the given values is invalid.\">\n        <source>One or more of the given values is invalid.</source>\n        <target>Einer oder mehrere der angegebenen Werte sind ungültig.</target>\n      </trans-unit>\n      <trans-unit id=\"3NeQftv\" resname=\"This field was not expected.\">\n        <source>This field was not expected.</source>\n        <target>Dieses Feld wurde nicht erwartet.</target>\n      </trans-unit>\n      <trans-unit id=\"SwMV4zp\" resname=\"This field is missing.\">\n        <source>This field is missing.</source>\n        <target>Dieses Feld fehlt.</target>\n      </trans-unit>\n      <trans-unit id=\"LO2vFKN\" resname=\"This value is not a valid date.\">\n        <source>This value is not a valid date.</source>\n        <target>Dieser Wert ist kein gültiges Datum.</target>\n      </trans-unit>\n      <trans-unit id=\"86dU_nv\" resname=\"This value is not a valid datetime.\">\n        <source>This value is not a valid datetime.</source>\n        <target>Dieser Wert ist keine gültige Zeitangabe.</target>\n      </trans-unit>\n      <trans-unit id=\"PSvNXdi\" resname=\"This value is not a valid email address.\">\n        <source>This value is not a valid email address.</source>\n        <target>Dieser Wert ist keine gültige E-Mail-Adresse.</target>\n      </trans-unit>\n      <trans-unit id=\"3KeHbZy\" resname=\"The file could not be found.\">\n        <source>The file could not be found.</source>\n        <target>Die Datei wurde nicht gefunden.</target>\n      </trans-unit>\n      <trans-unit id=\"KtJhQZo\" resname=\"The file is not readable.\">\n        <source>The file is not readable.</source>\n        <target>Die Datei konnte nicht gelesen werden.</target>\n      </trans-unit>\n      <trans-unit id=\"JocOVM2\" resname=\"The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.\">\n        <source>The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.</source>\n        <target>Die Datei ist zu groß ({{ size }} {{ suffix }}). Die zulässige Höchstgröße ist {{ limit }} {{ Suffix }}.</target>\n      </trans-unit>\n      <trans-unit id=\"YW21SPH\" resname=\"The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.\">\n        <source>The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.</source>\n        <target>Der Mime-Typ der Datei ist ungültig ({{ type }}). Erlaubte Mime-Typen sind {{ types }}.</target>\n      </trans-unit>\n      <trans-unit id=\"NubOmrs\" resname=\"This value should be {{ limit }} or less.\">\n        <source>This value should be {{ limit }} or less.</source>\n        <target>Dieser Wert sollte {{ limit }} oder weniger betragen.</target>\n      </trans-unit>\n      <trans-unit id=\"HX7TOFm\" resname=\"This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.\">\n        <source>This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.</source>\n        <target>Dieser Wert ist zu lang. Er sollte {{ limit }} Zeichen oder weniger haben.|Dieser Wert ist zu lang. Er sollte {{ limit }} Zeichen oder weniger haben.</target>\n      </trans-unit>\n      <trans-unit id=\"qgR8M_U\" resname=\"This value should be {{ limit }} or more.\">\n        <source>This value should be {{ limit }} or more.</source>\n        <target>Dieser Wert sollte {{ limit }} oder mehr betragen.</target>\n      </trans-unit>\n      <trans-unit id=\"ekfrU.c\" resname=\"This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.\">\n        <source>This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.</source>\n        <target>Dieser Wert ist zu kurz. Er sollte {{ limit }} Zeichen oder mehr haben.|Dieser Wert ist zu kurz. Er sollte {{ limit }} Zeichen oder mehr haben.</target>\n      </trans-unit>\n      <trans-unit id=\"1KV4L.t\" resname=\"This value should not be blank.\">\n        <source>This value should not be blank.</source>\n        <target>Dieser Wert darf nicht leer sein.</target>\n      </trans-unit>\n      <trans-unit id=\"2G4Vepm\" resname=\"This value should not be null.\">\n        <source>This value should not be null.</source>\n        <target>Dieser Wert sollte nicht \"null\" sein.</target>\n      </trans-unit>\n      <trans-unit id=\"yDc3m6E\" resname=\"This value should be null.\">\n        <source>This value should be null.</source>\n        <target>Dieser Wert sollte \"null\" sein.</target>\n      </trans-unit>\n      <trans-unit id=\"zKzWejA\" resname=\"This value is not valid.\">\n        <source>This value is not valid.</source>\n        <target>Dieser Wert ist ungültig.</target>\n      </trans-unit>\n      <trans-unit id=\"HSuBZpQ\" resname=\"This value is not a valid time.\">\n        <source>This value is not a valid time.</source>\n        <target>Dieser Wert ist keine gültige Uhrzeit.</target>\n      </trans-unit>\n      <trans-unit id=\"snWc_QT\" resname=\"This value is not a valid URL.\">\n        <source>This value is not a valid URL.</source>\n        <target>Dieser Wert ist keine gültige URL.</target>\n      </trans-unit>\n      <trans-unit id=\"jpaLsb2\" resname=\"The two values should be equal.\">\n        <source>The two values should be equal.</source>\n        <target>Die beiden Werte sollten gleich sein.</target>\n      </trans-unit>\n      <trans-unit id=\"fIlB1B_\" resname=\"The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.\">\n        <source>The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.</source>\n        <target>Die Datei ist zu groß. Erlaubte maximale Größe ist {{ limit }} {{ suffix }}.</target>\n      </trans-unit>\n      <trans-unit id=\"tW7o0t9\" resname=\"The file is too large.\">\n        <source>The file is too large.</source>\n        <target>Die Datei ist zu groß.</target>\n      </trans-unit>\n      <trans-unit id=\".exF5Ww\" resname=\"The file could not be uploaded.\">\n        <source>The file could not be uploaded.</source>\n        <target>Die Datei konnte nicht hochgeladen werden.</target>\n      </trans-unit>\n      <trans-unit id=\"d7sS5yw\" resname=\"This value should be a valid number.\">\n        <source>This value should be a valid number.</source>\n        <target>Dieser Wert sollte eine gültige Zahl sein.</target>\n      </trans-unit>\n      <trans-unit id=\"BS2Ez6i\" resname=\"This file is not a valid image.\">\n        <source>This file is not a valid image.</source>\n        <target>Diese Datei ist kein gültiges Bild.</target>\n      </trans-unit>\n      <trans-unit id=\"ydcT9kU\" resname=\"This is not a valid IP address.\">\n        <source>This is not a valid IP address.</source>\n        <target>Das ist keine gültige IP-Adresse.</target>\n      </trans-unit>\n      <trans-unit id=\"lDOGNFX\" resname=\"This value is not a valid language.\">\n        <source>This value is not a valid language.</source>\n        <target>Dieser Wert ist keine gültige Sprache.</target>\n      </trans-unit>\n      <trans-unit id=\"y9IdYkA\" resname=\"This value is not a valid locale.\">\n        <source>This value is not a valid locale.</source>\n        <target>Dieser Wert ist kein gültiges Gebietsschema.</target>\n      </trans-unit>\n      <trans-unit id=\"1YC0pOd\" resname=\"This value is not a valid country.\">\n        <source>This value is not a valid country.</source>\n        <target>Dieser Wert ist kein gültiges Land.</target>\n      </trans-unit>\n      <trans-unit id=\"B5ebaMp\" resname=\"This value is already used.\">\n        <source>This value is already used.</source>\n        <target>Dieser Wert wird bereits verwendet.</target>\n      </trans-unit>\n      <trans-unit id=\"L6097a6\" resname=\"The size of the image could not be detected.\">\n        <source>The size of the image could not be detected.</source>\n        <target>Die Größe des Bildes konnte nicht ermittelt werden.</target>\n      </trans-unit>\n      <trans-unit id=\"zVtJJEa\" resname=\"The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.\">\n        <source>The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.</source>\n        <target>Die Bildbreite ist zu groß ({{ width }}px). Erlaubte maximale Breite ist {{ max_width }}px.</target>\n      </trans-unit>\n      <trans-unit id=\"s8LFQGC\" resname=\"The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.\">\n        <source>The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.</source>\n        <target>Die Bildbreite ist zu klein ({{ width }}px). Die erwartete Mindestbreite ist {{ min_width }}px.</target>\n      </trans-unit>\n      <trans-unit id=\"Z.NgqFj\" resname=\"The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.\">\n        <source>The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.</source>\n        <target>Die Bildhöhe ist zu groß ({{ height }}px). Erlaubte maximale Höhe ist {{ max_height }}px.</target>\n      </trans-unit>\n      <trans-unit id=\"AW1lWVM\" resname=\"The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.\">\n        <source>The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.</source>\n        <target>Die Bildhöhe ist zu klein ({{ height }}px). Die erwartete Mindesthöhe ist {{ min_height }}px.</target>\n      </trans-unit>\n      <trans-unit id=\"PSdMNab\" resname=\"This value should be the user's current password.\">\n        <source>This value should be the user's current password.</source>\n        <target>Dieser Wert sollte das aktuelle Passwort des Benutzers sein.</target>\n      </trans-unit>\n      <trans-unit id=\"gYImVyV\" resname=\"This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.\">\n        <source>This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.</source>\n        <target>Dieser Wert sollte genau {{ Grenzwert }} Zeichen haben.|Dieser Wert sollte genau {{ Grenzwert }} Zeichen haben.</target>\n      </trans-unit>\n      <trans-unit id=\"xJ2Bcr_\" resname=\"The file was only partially uploaded.\">\n        <source>The file was only partially uploaded.</source>\n        <target>Die Datei wurde nur teilweise hochgeladen.</target>\n      </trans-unit>\n      <trans-unit id=\"HzkJDtF\" resname=\"No file was uploaded.\">\n        <source>No file was uploaded.</source>\n        <target>Es wurde keine Datei hochgeladen.</target>\n      </trans-unit>\n      <trans-unit id=\"mHfEaB3\" resname=\"No temporary folder was configured in php.ini.\">\n        <source>No temporary folder was configured in php.ini.</source>\n        <target>In der php.ini wurde kein temporärer Ordner konfiguriert, oder der konfigurierte Ordner existiert nicht.</target>\n      </trans-unit>\n      <trans-unit id=\"y9K3BGb\" resname=\"Cannot write temporary file to disk.\">\n        <source>Cannot write temporary file to disk.</source>\n        <target>Temporäre Datei kann nicht auf die Festplatte geschrieben werden.</target>\n      </trans-unit>\n      <trans-unit id=\"kx3yHIM\" resname=\"A PHP extension caused the upload to fail.\">\n        <source>A PHP extension caused the upload to fail.</source>\n        <target>Wegen einer PHP-Erweiterung ist das Hochladen gescheitert.</target>\n      </trans-unit>\n      <trans-unit id=\"gTJYRl6\" resname=\"This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.\">\n        <source>This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.</source>\n        <target>Diese Sammlung sollte {{ limit }} Elemente oder mehr enthalten.|Diese Sammlung sollte {{ limit }} Elemente oder mehr enthalten.</target>\n      </trans-unit>\n      <trans-unit id=\"FFn3lVn\" resname=\"This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.\">\n        <source>This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.</source>\n        <target>Diese Sammlung sollte {{ limit }} Element oder weniger enthalten.|Diese Sammlung sollte {{ limit }} Elemente oder weniger enthalten.</target>\n      </trans-unit>\n      <trans-unit id=\"bSdilZv\" resname=\"This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.\">\n        <source>This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.</source>\n        <target>Diese Sammlung sollte genau {{ Limit }} Elemente enthalten.|Diese Sammlung sollte genau {{ Limit }} Elemente enthalten.</target>\n      </trans-unit>\n      <trans-unit id=\"MAzmID7\" resname=\"Invalid card number.\">\n        <source>Invalid card number.</source>\n        <target>Ungültige Kartennummer.</target>\n      </trans-unit>\n      <trans-unit id=\"c3REGK3\" resname=\"Unsupported card type or invalid card number.\">\n        <source>Unsupported card type or invalid card number.</source>\n        <target>Nicht unterstützter Kartentyp oder ungültige Kartennummer.</target>\n      </trans-unit>\n      <trans-unit id=\"XSVzcbV\" resname=\"This is not a valid International Bank Account Number (IBAN).\">\n        <source>This is not a valid International Bank Account Number (IBAN).</source>\n        <target>Das ist keine gültige internationale Bankkontonummer (IBAN).</target>\n      </trans-unit>\n      <trans-unit id=\"yHirwNr\" resname=\"This value is not a valid ISBN-10.\">\n        <source>This value is not a valid ISBN-10.</source>\n        <target>Dieser Wert ist keine gültige ISBN-10.</target>\n      </trans-unit>\n      <trans-unit id=\"c_q0_ua\" resname=\"This value is not a valid ISBN-13.\">\n        <source>This value is not a valid ISBN-13.</source>\n        <target>Dieser Wert ist keine gültige ISBN-13.</target>\n      </trans-unit>\n      <trans-unit id=\"M4FlD6n\" resname=\"This value is neither a valid ISBN-10 nor a valid ISBN-13.\">\n        <source>This value is neither a valid ISBN-10 nor a valid ISBN-13.</source>\n        <target>Dieser Wert ist weder eine gültige ISBN-10 noch eine gültige ISBN-13.</target>\n      </trans-unit>\n      <trans-unit id=\"cuct0Ow\" resname=\"This value is not a valid ISSN.\">\n        <source>This value is not a valid ISSN.</source>\n        <target>Dieser Wert ist keine gültige ISSN.</target>\n      </trans-unit>\n      <trans-unit id=\"JBLs1a1\" resname=\"This value is not a valid currency.\">\n        <source>This value is not a valid currency.</source>\n        <target>Dieser Wert ist keine gültige Währung.</target>\n      </trans-unit>\n      <trans-unit id=\"c.WxzFW\" resname=\"This value should be equal to {{ compared_value }}.\">\n        <source>This value should be equal to {{ compared_value }}.</source>\n        <target>Dieser Wert sollte gleich {{ compared_value }} sein.</target>\n      </trans-unit>\n      <trans-unit id=\"_jdjkwq\" resname=\"This value should be greater than {{ compared_value }}.\">\n        <source>This value should be greater than {{ compared_value }}.</source>\n        <target>Dieser Wert sollte größer {{ compared_value }} sein.</target>\n      </trans-unit>\n      <trans-unit id=\"o8A8a0H\" resname=\"This value should be greater than or equal to {{ compared_value }}.\">\n        <source>This value should be greater than or equal to {{ compared_value }}.</source>\n        <target>Dieser Wert sollte größer oder gleich {{ compared_value }} sein.</target>\n      </trans-unit>\n      <trans-unit id=\"bOF1fpm\" resname=\"This value should be identical to {{ compared_value_type }} {{ compared_value }}.\">\n        <source>This value should be identical to {{ compared_value_type }} {{ compared_value }}.</source>\n        <target>Dieser Wert sollte identisch sein mit {{ compared_value_type }} {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"jG0QFKw\" resname=\"This value should be less than {{ compared_value }}.\">\n        <source>This value should be less than {{ compared_value }}.</source>\n        <target>Dieser Wert sollte kleiner als {{ compared_value }} sein.</target>\n      </trans-unit>\n      <trans-unit id=\"9lWrKmm\" resname=\"This value should be less than or equal to {{ compared_value }}.\">\n        <source>This value should be less than or equal to {{ compared_value }}.</source>\n        <target>Dieser Wert sollte gleich oder kleiner als {{ compared_value }} sein.</target>\n      </trans-unit>\n      <trans-unit id=\"ftTwGs.\" resname=\"This value should not be equal to {{ compared_value }}.\">\n        <source>This value should not be equal to {{ compared_value }}.</source>\n        <target>Dieser Wert sollte nicht gleich {{ compared_value }} sein.</target>\n      </trans-unit>\n      <trans-unit id=\"Bi22JLt\" resname=\"This value should not be identical to {{ compared_value_type }} {{ compared_value }}.\">\n        <source>This value should not be identical to {{ compared_value_type }} {{ compared_value }}.</source>\n        <target>Dieser Wert sollte nicht identisch sein mit {{ compared_value_type }} {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"VczCWzQ\" resname=\"The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.\">\n        <source>The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.</source>\n        <target>Das Bildverhältnis ist zu groß ({{ ratio }}). Das zulässige maximale Verhältnis ist {{ max_ratio }}.</target>\n      </trans-unit>\n      <trans-unit id=\"v57PXhq\" resname=\"The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.\">\n        <source>The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.</source>\n        <target>Das Bildverhältnis ist zu klein ({{ ratio }}). Das erwartete Mindestverhältnis ist {{ min_ratio }}.</target>\n      </trans-unit>\n      <trans-unit id=\"rpajj.a\" resname=\"The image is square ({{ width }}x{{ height }}px). Square images are not allowed.\">\n        <source>The image is square ({{ width }}x{{ height }}px). Square images are not allowed.</source>\n        <target>Das Bild ist quadratisch ({{ width }}x{{ height }}px). Quadratische Bilder sind nicht erlaubt.</target>\n      </trans-unit>\n      <trans-unit id=\"G_lu2qW\" resname=\"The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.\">\n        <source>The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.</source>\n        <target>Das Bild ist im Querformat ({{ width }}x{{ height }}px). Bilder im Querformat sind nicht erlaubt.</target>\n      </trans-unit>\n      <trans-unit id=\"sFyGx4B\" resname=\"The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.\">\n        <source>The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.</source>\n        <target>Das Bild ist im Hochformat ({{ width }}x{{ height }}px). Bilder im Hochformat sind nicht erlaubt.</target>\n      </trans-unit>\n      <trans-unit id=\"jZgqcpL\" resname=\"An empty file is not allowed.\">\n        <source>An empty file is not allowed.</source>\n        <target>Eine leere Datei ist nicht erlaubt.</target>\n      </trans-unit>\n      <trans-unit id=\"bcfVezI\" resname=\"The host could not be resolved.\">\n        <source>The host could not be resolved.</source>\n        <target>Der Host konnte nicht aufgelöst werden.</target>\n      </trans-unit>\n      <trans-unit id=\"NtzKvgt\" resname=\"This value does not match the expected {{ charset }} charset.\">\n        <source>This value does not match the expected {{ charset }} charset.</source>\n        <target>Dieser Wert stimmt nicht mit dem erwarteten {{ charset }} Zeichensatz überein.</target>\n      </trans-unit>\n      <trans-unit id=\"Wi2y9.N\" resname=\"This is not a valid Business Identifier Code (BIC).\">\n        <source>This is not a valid Business Identifier Code (BIC).</source>\n        <target>Dies ist kein gültiger Business Identifier Code (BIC).</target>\n      </trans-unit>\n      <trans-unit id=\"VKDowX6\" resname=\"Error\">\n        <source>Error</source>\n        <target>Fehler</target>\n      </trans-unit>\n      <trans-unit id=\"8zqt0Ik\" resname=\"This is not a valid UUID.\">\n        <source>This is not a valid UUID.</source>\n        <target>Dies ist keine gültige UUID.</target>\n      </trans-unit>\n      <trans-unit id=\"ru.4wkH\" resname=\"This value should be a multiple of {{ compared_value }}.\">\n        <source>This value should be a multiple of {{ compared_value }}.</source>\n        <target>Dieser Wert sollte ein Vielfaches von {{ compared_value }} sein.</target>\n      </trans-unit>\n      <trans-unit id=\"M3vyK6s\" resname=\"This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.\">\n        <source>This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.</source>\n        <target>Dieser Business Identifier Code (BIC) ist nicht mit der IBAN {{ iban }} verbunden.</target>\n      </trans-unit>\n      <trans-unit id=\"2v2xpAh\" resname=\"This value should be valid JSON.\">\n        <source>This value should be valid JSON.</source>\n        <target>Dieser Wert sollte gültiges JSON sein.</target>\n      </trans-unit>\n      <trans-unit id=\"9CWVEGq\" resname=\"This collection should contain only unique elements.\">\n        <source>This collection should contain only unique elements.</source>\n        <target>Diese Sammlung sollte nur eindeutige Elemente enthalten.</target>\n      </trans-unit>\n      <trans-unit id=\"WdvZfq.\" resname=\"This value should be positive.\">\n        <source>This value should be positive.</source>\n        <target>Dieser Wert sollte positiv sein.</target>\n      </trans-unit>\n      <trans-unit id=\"ubHMK2q\" resname=\"This value should be either positive or zero.\">\n        <source>This value should be either positive or zero.</source>\n        <target>Dieser Wert sollte entweder positiv oder Null sein.</target>\n      </trans-unit>\n      <trans-unit id=\"IwNTzo_\" resname=\"This value should be negative.\">\n        <source>This value should be negative.</source>\n        <target>Dieser Wert sollte negativ sein.</target>\n      </trans-unit>\n      <trans-unit id=\"0GfwMfP\" resname=\"This value should be either negative or zero.\">\n        <source>This value should be either negative or zero.</source>\n        <target>Dieser Wert sollte entweder negativ oder Null sein.</target>\n      </trans-unit>\n      <trans-unit id=\"fs3qQZR\" resname=\"This value is not a valid timezone.\">\n        <source>This value is not a valid timezone.</source>\n        <target>Dieser Wert ist keine gültige Zeitzone.</target>\n      </trans-unit>\n      <trans-unit id=\"40dnsod\" resname=\"This password has been leaked in a data breach, it must not be used. Please use another password.\">\n        <source>This password has been leaked in a data breach, it must not be used. Please use another password.</source>\n        <target>Dieses Passwort ist durch eine Datenpanne bekannt geworden und darf nicht mehr verwendet werden. Bitte verwenden Sie ein anderes Passwort.</target>\n      </trans-unit>\n      <trans-unit id=\"VvxxWas\" resname=\"This value should be between {{ min }} and {{ max }}.\">\n        <source>This value should be between {{ min }} and {{ max }}.</source>\n        <target>Dieser Wert sollte zwischen {{ min }} und {{ max }} liegen.</target>\n      </trans-unit>\n      <trans-unit id=\".SEaaBa\" resname=\"This form should not contain extra fields.\">\n        <source>This form should not contain extra fields.</source>\n        <target>Dieses Formular sollte keine zusätzlichen Felder enthalten.</target>\n      </trans-unit>\n      <trans-unit id=\"WPnLAh9\" resname=\"The uploaded file was too large. Please try to upload a smaller file.\">\n        <source>The uploaded file was too large. Please try to upload a smaller file.</source>\n        <target>Die hochgeladene Datei war zu groß. Bitte versuchen Sie, eine kleinere Datei hochzuladen.</target>\n      </trans-unit>\n      <trans-unit id=\"fvxWW3V\" resname=\"The CSRF token is invalid. Please try to resubmit the form.\">\n        <source>The CSRF token is invalid. Please try to resubmit the form.</source>\n        <target>Das CSRF-Token ist ungültig. Bitte versuchen Sie, das Formular erneut abzuschicken.</target>\n      </trans-unit>\n      <trans-unit id=\"9gDpv.9\" resname=\"form.uri.unique\">\n        <source>form.uri.unique</source>\n        <target>Diese URI wird bereits für diesen Auftraggeber verwendet. Bitte wählen Sie einen anderen.</target>\n      </trans-unit>\n    </body>\n  </file>\n</xliff>\n"
  },
  {
    "path": "translations/validators.en.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file source-language=\"en\" target-language=\"en\" datatype=\"plaintext\" original=\"file.ext\">\n    <header>\n      <tool tool-id=\"symfony\" tool-name=\"Symfony\"/>\n    </header>\n    <body>\n      <trans-unit id=\"zh5oKD9\" resname=\"This value should be false.\">\n        <source>This value should be false.</source>\n        <target>This value should be false.</target>\n      </trans-unit>\n      <trans-unit id=\"NN2_iru\" resname=\"This value should be true.\">\n        <source>This value should be true.</source>\n        <target>This value should be true.</target>\n      </trans-unit>\n      <trans-unit id=\"OjM_kpf\" resname=\"This value should be of type {{ type }}.\">\n        <source>This value should be of type {{ type }}.</source>\n        <target>This value should be of type {{ type }}.</target>\n      </trans-unit>\n      <trans-unit id=\"f0P5pBD\" resname=\"This value should be blank.\">\n        <source>This value should be blank.</source>\n        <target>This value should be blank.</target>\n      </trans-unit>\n      <trans-unit id=\"ih.4TRN\" resname=\"The value you selected is not a valid choice.\">\n        <source>The value you selected is not a valid choice.</source>\n        <target>The value you selected is not a valid choice.</target>\n      </trans-unit>\n      <trans-unit id=\"u81CkP8\" resname=\"You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.\">\n        <source>You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.</source>\n        <target>You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.</target>\n      </trans-unit>\n      <trans-unit id=\"nIvOQ_o\" resname=\"You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.\">\n        <source>You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.</source>\n        <target>You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.</target>\n      </trans-unit>\n      <trans-unit id=\"87zjiHi\" resname=\"One or more of the given values is invalid.\">\n        <source>One or more of the given values is invalid.</source>\n        <target>One or more of the given values is invalid.</target>\n      </trans-unit>\n      <trans-unit id=\"3NeQftv\" resname=\"This field was not expected.\">\n        <source>This field was not expected.</source>\n        <target>This field was not expected.</target>\n      </trans-unit>\n      <trans-unit id=\"SwMV4zp\" resname=\"This field is missing.\">\n        <source>This field is missing.</source>\n        <target>This field is missing.</target>\n      </trans-unit>\n      <trans-unit id=\"LO2vFKN\" resname=\"This value is not a valid date.\">\n        <source>This value is not a valid date.</source>\n        <target>This value is not a valid date.</target>\n      </trans-unit>\n      <trans-unit id=\"86dU_nv\" resname=\"This value is not a valid datetime.\">\n        <source>This value is not a valid datetime.</source>\n        <target>This value is not a valid datetime.</target>\n      </trans-unit>\n      <trans-unit id=\"PSvNXdi\" resname=\"This value is not a valid email address.\">\n        <source>This value is not a valid email address.</source>\n        <target>This value is not a valid email address.</target>\n      </trans-unit>\n      <trans-unit id=\"3KeHbZy\" resname=\"The file could not be found.\">\n        <source>The file could not be found.</source>\n        <target>The file could not be found.</target>\n      </trans-unit>\n      <trans-unit id=\"KtJhQZo\" resname=\"The file is not readable.\">\n        <source>The file is not readable.</source>\n        <target>The file is not readable.</target>\n      </trans-unit>\n      <trans-unit id=\"JocOVM2\" resname=\"The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.\">\n        <source>The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.</source>\n        <target>The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.</target>\n      </trans-unit>\n      <trans-unit id=\"YW21SPH\" resname=\"The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.\">\n        <source>The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.</source>\n        <target>The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.</target>\n      </trans-unit>\n      <trans-unit id=\"NubOmrs\" resname=\"This value should be {{ limit }} or less.\">\n        <source>This value should be {{ limit }} or less.</source>\n        <target>This value should be {{ limit }} or less.</target>\n      </trans-unit>\n      <trans-unit id=\"HX7TOFm\" resname=\"This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.\">\n        <source>This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.</source>\n        <target>This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.</target>\n      </trans-unit>\n      <trans-unit id=\"qgR8M_U\" resname=\"This value should be {{ limit }} or more.\">\n        <source>This value should be {{ limit }} or more.</source>\n        <target>This value should be {{ limit }} or more.</target>\n      </trans-unit>\n      <trans-unit id=\"ekfrU.c\" resname=\"This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.\">\n        <source>This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.</source>\n        <target>This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.</target>\n      </trans-unit>\n      <trans-unit id=\"1KV4L.t\" resname=\"This value should not be blank.\">\n        <source>This value should not be blank.</source>\n        <target>This value should not be blank.</target>\n      </trans-unit>\n      <trans-unit id=\"2G4Vepm\" resname=\"This value should not be null.\">\n        <source>This value should not be null.</source>\n        <target>This value should not be null.</target>\n      </trans-unit>\n      <trans-unit id=\"yDc3m6E\" resname=\"This value should be null.\">\n        <source>This value should be null.</source>\n        <target>This value should be null.</target>\n      </trans-unit>\n      <trans-unit id=\"zKzWejA\" resname=\"This value is not valid.\">\n        <source>This value is not valid.</source>\n        <target>This value is not valid.</target>\n      </trans-unit>\n      <trans-unit id=\"HSuBZpQ\" resname=\"This value is not a valid time.\">\n        <source>This value is not a valid time.</source>\n        <target>This value is not a valid time.</target>\n      </trans-unit>\n      <trans-unit id=\"snWc_QT\" resname=\"This value is not a valid URL.\">\n        <source>This value is not a valid URL.</source>\n        <target>This value is not a valid URL.</target>\n      </trans-unit>\n      <trans-unit id=\"jpaLsb2\" resname=\"The two values should be equal.\">\n        <source>The two values should be equal.</source>\n        <target>The two values should be equal.</target>\n      </trans-unit>\n      <trans-unit id=\"fIlB1B_\" resname=\"The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.\">\n        <source>The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.</source>\n        <target>The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.</target>\n      </trans-unit>\n      <trans-unit id=\"tW7o0t9\" resname=\"The file is too large.\">\n        <source>The file is too large.</source>\n        <target>The file is too large.</target>\n      </trans-unit>\n      <trans-unit id=\".exF5Ww\" resname=\"The file could not be uploaded.\">\n        <source>The file could not be uploaded.</source>\n        <target>The file could not be uploaded.</target>\n      </trans-unit>\n      <trans-unit id=\"d7sS5yw\" resname=\"This value should be a valid number.\">\n        <source>This value should be a valid number.</source>\n        <target>This value should be a valid number.</target>\n      </trans-unit>\n      <trans-unit id=\"BS2Ez6i\" resname=\"This file is not a valid image.\">\n        <source>This file is not a valid image.</source>\n        <target>This file is not a valid image.</target>\n      </trans-unit>\n      <trans-unit id=\"ydcT9kU\" resname=\"This is not a valid IP address.\">\n        <source>This is not a valid IP address.</source>\n        <target>This is not a valid IP address.</target>\n      </trans-unit>\n      <trans-unit id=\"lDOGNFX\" resname=\"This value is not a valid language.\">\n        <source>This value is not a valid language.</source>\n        <target>This value is not a valid language.</target>\n      </trans-unit>\n      <trans-unit id=\"y9IdYkA\" resname=\"This value is not a valid locale.\">\n        <source>This value is not a valid locale.</source>\n        <target>This value is not a valid locale.</target>\n      </trans-unit>\n      <trans-unit id=\"1YC0pOd\" resname=\"This value is not a valid country.\">\n        <source>This value is not a valid country.</source>\n        <target>This value is not a valid country.</target>\n      </trans-unit>\n      <trans-unit id=\"B5ebaMp\" resname=\"This value is already used.\">\n        <source>This value is already used.</source>\n        <target>This value is already used.</target>\n      </trans-unit>\n      <trans-unit id=\"L6097a6\" resname=\"The size of the image could not be detected.\">\n        <source>The size of the image could not be detected.</source>\n        <target>The size of the image could not be detected.</target>\n      </trans-unit>\n      <trans-unit id=\"zVtJJEa\" resname=\"The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.\">\n        <source>The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.</source>\n        <target>The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.</target>\n      </trans-unit>\n      <trans-unit id=\"s8LFQGC\" resname=\"The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.\">\n        <source>The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.</source>\n        <target>The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.</target>\n      </trans-unit>\n      <trans-unit id=\"Z.NgqFj\" resname=\"The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.\">\n        <source>The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.</source>\n        <target>The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.</target>\n      </trans-unit>\n      <trans-unit id=\"AW1lWVM\" resname=\"The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.\">\n        <source>The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.</source>\n        <target>The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.</target>\n      </trans-unit>\n      <trans-unit id=\"PSdMNab\" resname=\"This value should be the user's current password.\">\n        <source>This value should be the user's current password.</source>\n        <target>This value should be the user's current password.</target>\n      </trans-unit>\n      <trans-unit id=\"gYImVyV\" resname=\"This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.\">\n        <source>This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.</source>\n        <target>This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.</target>\n      </trans-unit>\n      <trans-unit id=\"xJ2Bcr_\" resname=\"The file was only partially uploaded.\">\n        <source>The file was only partially uploaded.</source>\n        <target>The file was only partially uploaded.</target>\n      </trans-unit>\n      <trans-unit id=\"HzkJDtF\" resname=\"No file was uploaded.\">\n        <source>No file was uploaded.</source>\n        <target>No file was uploaded.</target>\n      </trans-unit>\n      <trans-unit id=\"mHfEaB3\" resname=\"No temporary folder was configured in php.ini.\">\n        <source>No temporary folder was configured in php.ini.</source>\n        <target>No temporary folder was configured in php.ini, or the configured folder does not exist.</target>\n      </trans-unit>\n      <trans-unit id=\"y9K3BGb\" resname=\"Cannot write temporary file to disk.\">\n        <source>Cannot write temporary file to disk.</source>\n        <target>Cannot write temporary file to disk.</target>\n      </trans-unit>\n      <trans-unit id=\"kx3yHIM\" resname=\"A PHP extension caused the upload to fail.\">\n        <source>A PHP extension caused the upload to fail.</source>\n        <target>A PHP extension caused the upload to fail.</target>\n      </trans-unit>\n      <trans-unit id=\"gTJYRl6\" resname=\"This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.\">\n        <source>This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.</source>\n        <target>This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.</target>\n      </trans-unit>\n      <trans-unit id=\"FFn3lVn\" resname=\"This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.\">\n        <source>This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.</source>\n        <target>This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.</target>\n      </trans-unit>\n      <trans-unit id=\"bSdilZv\" resname=\"This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.\">\n        <source>This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.</source>\n        <target>This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.</target>\n      </trans-unit>\n      <trans-unit id=\"MAzmID7\" resname=\"Invalid card number.\">\n        <source>Invalid card number.</source>\n        <target>Invalid card number.</target>\n      </trans-unit>\n      <trans-unit id=\"c3REGK3\" resname=\"Unsupported card type or invalid card number.\">\n        <source>Unsupported card type or invalid card number.</source>\n        <target>Unsupported card type or invalid card number.</target>\n      </trans-unit>\n      <trans-unit id=\"XSVzcbV\" resname=\"This is not a valid International Bank Account Number (IBAN).\">\n        <source>This is not a valid International Bank Account Number (IBAN).</source>\n        <target>This is not a valid International Bank Account Number (IBAN).</target>\n      </trans-unit>\n      <trans-unit id=\"yHirwNr\" resname=\"This value is not a valid ISBN-10.\">\n        <source>This value is not a valid ISBN-10.</source>\n        <target>This value is not a valid ISBN-10.</target>\n      </trans-unit>\n      <trans-unit id=\"c_q0_ua\" resname=\"This value is not a valid ISBN-13.\">\n        <source>This value is not a valid ISBN-13.</source>\n        <target>This value is not a valid ISBN-13.</target>\n      </trans-unit>\n      <trans-unit id=\"M4FlD6n\" resname=\"This value is neither a valid ISBN-10 nor a valid ISBN-13.\">\n        <source>This value is neither a valid ISBN-10 nor a valid ISBN-13.</source>\n        <target>This value is neither a valid ISBN-10 nor a valid ISBN-13.</target>\n      </trans-unit>\n      <trans-unit id=\"cuct0Ow\" resname=\"This value is not a valid ISSN.\">\n        <source>This value is not a valid ISSN.</source>\n        <target>This value is not a valid ISSN.</target>\n      </trans-unit>\n      <trans-unit id=\"JBLs1a1\" resname=\"This value is not a valid currency.\">\n        <source>This value is not a valid currency.</source>\n        <target>This value is not a valid currency.</target>\n      </trans-unit>\n      <trans-unit id=\"c.WxzFW\" resname=\"This value should be equal to {{ compared_value }}.\">\n        <source>This value should be equal to {{ compared_value }}.</source>\n        <target>This value should be equal to {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"_jdjkwq\" resname=\"This value should be greater than {{ compared_value }}.\">\n        <source>This value should be greater than {{ compared_value }}.</source>\n        <target>This value should be greater than {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"o8A8a0H\" resname=\"This value should be greater than or equal to {{ compared_value }}.\">\n        <source>This value should be greater than or equal to {{ compared_value }}.</source>\n        <target>This value should be greater than or equal to {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"bOF1fpm\" resname=\"This value should be identical to {{ compared_value_type }} {{ compared_value }}.\">\n        <source>This value should be identical to {{ compared_value_type }} {{ compared_value }}.</source>\n        <target>This value should be identical to {{ compared_value_type }} {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"jG0QFKw\" resname=\"This value should be less than {{ compared_value }}.\">\n        <source>This value should be less than {{ compared_value }}.</source>\n        <target>This value should be less than {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"9lWrKmm\" resname=\"This value should be less than or equal to {{ compared_value }}.\">\n        <source>This value should be less than or equal to {{ compared_value }}.</source>\n        <target>This value should be less than or equal to {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"ftTwGs.\" resname=\"This value should not be equal to {{ compared_value }}.\">\n        <source>This value should not be equal to {{ compared_value }}.</source>\n        <target>This value should not be equal to {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"Bi22JLt\" resname=\"This value should not be identical to {{ compared_value_type }} {{ compared_value }}.\">\n        <source>This value should not be identical to {{ compared_value_type }} {{ compared_value }}.</source>\n        <target>This value should not be identical to {{ compared_value_type }} {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"VczCWzQ\" resname=\"The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.\">\n        <source>The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.</source>\n        <target>The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.</target>\n      </trans-unit>\n      <trans-unit id=\"v57PXhq\" resname=\"The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.\">\n        <source>The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.</source>\n        <target>The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.</target>\n      </trans-unit>\n      <trans-unit id=\"rpajj.a\" resname=\"The image is square ({{ width }}x{{ height }}px). Square images are not allowed.\">\n        <source>The image is square ({{ width }}x{{ height }}px). Square images are not allowed.</source>\n        <target>The image is square ({{ width }}x{{ height }}px). Square images are not allowed.</target>\n      </trans-unit>\n      <trans-unit id=\"G_lu2qW\" resname=\"The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.\">\n        <source>The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.</source>\n        <target>The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.</target>\n      </trans-unit>\n      <trans-unit id=\"sFyGx4B\" resname=\"The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.\">\n        <source>The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.</source>\n        <target>The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.</target>\n      </trans-unit>\n      <trans-unit id=\"jZgqcpL\" resname=\"An empty file is not allowed.\">\n        <source>An empty file is not allowed.</source>\n        <target>An empty file is not allowed.</target>\n      </trans-unit>\n      <trans-unit id=\"bcfVezI\" resname=\"The host could not be resolved.\">\n        <source>The host could not be resolved.</source>\n        <target>The host could not be resolved.</target>\n      </trans-unit>\n      <trans-unit id=\"NtzKvgt\" resname=\"This value does not match the expected {{ charset }} charset.\">\n        <source>This value does not match the expected {{ charset }} charset.</source>\n        <target>This value does not match the expected {{ charset }} charset.</target>\n      </trans-unit>\n      <trans-unit id=\"Wi2y9.N\" resname=\"This is not a valid Business Identifier Code (BIC).\">\n        <source>This is not a valid Business Identifier Code (BIC).</source>\n        <target>This is not a valid Business Identifier Code (BIC).</target>\n      </trans-unit>\n      <trans-unit id=\"VKDowX6\" resname=\"Error\">\n        <source>Error</source>\n        <target>Error</target>\n      </trans-unit>\n      <trans-unit id=\"8zqt0Ik\" resname=\"This is not a valid UUID.\">\n        <source>This is not a valid UUID.</source>\n        <target>This is not a valid UUID.</target>\n      </trans-unit>\n      <trans-unit id=\"ru.4wkH\" resname=\"This value should be a multiple of {{ compared_value }}.\">\n        <source>This value should be a multiple of {{ compared_value }}.</source>\n        <target>This value should be a multiple of {{ compared_value }}.</target>\n      </trans-unit>\n      <trans-unit id=\"M3vyK6s\" resname=\"This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.\">\n        <source>This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.</source>\n        <target>This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.</target>\n      </trans-unit>\n      <trans-unit id=\"2v2xpAh\" resname=\"This value should be valid JSON.\">\n        <source>This value should be valid JSON.</source>\n        <target>This value should be valid JSON.</target>\n      </trans-unit>\n      <trans-unit id=\"9CWVEGq\" resname=\"This collection should contain only unique elements.\">\n        <source>This collection should contain only unique elements.</source>\n        <target>This collection should contain only unique elements.</target>\n      </trans-unit>\n      <trans-unit id=\"WdvZfq.\" resname=\"This value should be positive.\">\n        <source>This value should be positive.</source>\n        <target>This value should be positive.</target>\n      </trans-unit>\n      <trans-unit id=\"ubHMK2q\" resname=\"This value should be either positive or zero.\">\n        <source>This value should be either positive or zero.</source>\n        <target>This value should be either positive or zero.</target>\n      </trans-unit>\n      <trans-unit id=\"IwNTzo_\" resname=\"This value should be negative.\">\n        <source>This value should be negative.</source>\n        <target>This value should be negative.</target>\n      </trans-unit>\n      <trans-unit id=\"0GfwMfP\" resname=\"This value should be either negative or zero.\">\n        <source>This value should be either negative or zero.</source>\n        <target>This value should be either negative or zero.</target>\n      </trans-unit>\n      <trans-unit id=\"fs3qQZR\" resname=\"This value is not a valid timezone.\">\n        <source>This value is not a valid timezone.</source>\n        <target>This value is not a valid timezone.</target>\n      </trans-unit>\n      <trans-unit id=\"40dnsod\" resname=\"This password has been leaked in a data breach, it must not be used. Please use another password.\">\n        <source>This password has been leaked in a data breach, it must not be used. Please use another password.</source>\n        <target>This password has been leaked in a data breach, it must not be used. Please use another password.</target>\n      </trans-unit>\n      <trans-unit id=\"VvxxWas\" resname=\"This value should be between {{ min }} and {{ max }}.\">\n        <source>This value should be between {{ min }} and {{ max }}.</source>\n        <target>This value should be between {{ min }} and {{ max }}.</target>\n      </trans-unit>\n      <trans-unit id=\".SEaaBa\" resname=\"This form should not contain extra fields.\">\n        <source>This form should not contain extra fields.</source>\n        <target>This form should not contain extra fields.</target>\n      </trans-unit>\n      <trans-unit id=\"WPnLAh9\" resname=\"The uploaded file was too large. Please try to upload a smaller file.\">\n        <source>The uploaded file was too large. Please try to upload a smaller file.</source>\n        <target>The uploaded file was too large. Please try to upload a smaller file.</target>\n      </trans-unit>\n      <trans-unit id=\"fvxWW3V\" resname=\"The CSRF token is invalid. Please try to resubmit the form.\">\n        <source>The CSRF token is invalid. Please try to resubmit the form.</source>\n        <target>The CSRF token is invalid. Please try to resubmit the form.</target>\n      </trans-unit>\n      <trans-unit id=\"9gDpv.9\" resname=\"form.uri.unique\">\n        <source>form.uri.unique</source>\n        <target>This URI is already used with this principal. Please choose another one.</target>\n      </trans-unit>\n    </body>\n  </file>\n</xliff>\n"
  },
  {
    "path": "translations/validators.fr.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n    <file source-language=\"en\" target-language=\"fr\" datatype=\"plaintext\" original=\"file.ext\">\n        <body>\n            <trans-unit id=\"1\">\n                <source>This value should be false.</source>\n                <target>Cette valeur doit être fausse.</target>\n            </trans-unit>\n            <trans-unit id=\"2\">\n                <source>This value should be true.</source>\n                <target>Cette valeur doit être vraie.</target>\n            </trans-unit>\n            <trans-unit id=\"3\">\n                <source>This value should be of type {{ type }}.</source>\n                <target>Cette valeur doit être de type {{ type }}.</target>\n            </trans-unit>\n            <trans-unit id=\"4\">\n                <source>This value should be blank.</source>\n                <target>Cette valeur doit être vide.</target>\n            </trans-unit>\n            <trans-unit id=\"5\">\n                <source>The value you selected is not a valid choice.</source>\n                <target>Cette valeur doit être l'un des choix proposés.</target>\n            </trans-unit>\n            <trans-unit id=\"6\">\n                <source>You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.</source>\n                <target>Vous devez sélectionner au moins {{ limit }} choix.|Vous devez sélectionner au moins {{ limit }} choix.</target>\n            </trans-unit>\n            <trans-unit id=\"7\">\n                <source>You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.</source>\n                <target>Vous devez sélectionner au maximum {{ limit }} choix.|Vous devez sélectionner au maximum {{ limit }} choix.</target>\n            </trans-unit>\n            <trans-unit id=\"8\">\n                <source>One or more of the given values is invalid.</source>\n                <target>Une ou plusieurs des valeurs soumises sont invalides.</target>\n            </trans-unit>\n            <trans-unit id=\"9\">\n                <source>This field was not expected.</source>\n                <target>Ce champ n'a pas été prévu.</target>\n            </trans-unit>\n            <trans-unit id=\"10\">\n                <source>This field is missing.</source>\n                <target>Ce champ est manquant.</target>\n            </trans-unit>\n            <trans-unit id=\"11\">\n                <source>This value is not a valid date.</source>\n                <target>Cette valeur n'est pas une date valide.</target>\n            </trans-unit>\n            <trans-unit id=\"12\">\n                <source>This value is not a valid datetime.</source>\n                <target>Cette valeur n'est pas une date valide.</target>\n            </trans-unit>\n            <trans-unit id=\"13\">\n                <source>This value is not a valid email address.</source>\n                <target>Cette valeur n'est pas une adresse email valide.</target>\n            </trans-unit>\n            <trans-unit id=\"14\">\n                <source>The file could not be found.</source>\n                <target>Le fichier n'a pas été trouvé.</target>\n            </trans-unit>\n            <trans-unit id=\"15\">\n                <source>The file is not readable.</source>\n                <target>Le fichier n'est pas lisible.</target>\n            </trans-unit>\n            <trans-unit id=\"16\">\n                <source>The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.</source>\n                <target>Le fichier est trop volumineux ({{ size }} {{ suffix }}). Sa taille ne doit pas dépasser {{ limit }} {{ suffix }}.</target>\n            </trans-unit>\n            <trans-unit id=\"17\">\n                <source>The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.</source>\n                <target>Le type du fichier est invalide ({{ type }}). Les types autorisés sont {{ types }}.</target>\n            </trans-unit>\n            <trans-unit id=\"18\">\n                <source>This value should be {{ limit }} or less.</source>\n                <target>Cette valeur doit être inférieure ou égale à {{ limit }}.</target>\n            </trans-unit>\n            <trans-unit id=\"19\">\n                <source>This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.</source>\n                <target>Cette chaîne est trop longue. Elle doit avoir au maximum {{ limit }} caractère.|Cette chaîne est trop longue. Elle doit avoir au maximum {{ limit }} caractères.</target>\n            </trans-unit>\n            <trans-unit id=\"20\">\n                <source>This value should be {{ limit }} or more.</source>\n                <target>Cette valeur doit être supérieure ou égale à {{ limit }}.</target>\n            </trans-unit>\n            <trans-unit id=\"21\">\n                <source>This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.</source>\n                <target>Cette chaîne est trop courte. Elle doit avoir au minimum {{ limit }} caractère.|Cette chaîne est trop courte. Elle doit avoir au minimum {{ limit }} caractères.</target>\n            </trans-unit>\n            <trans-unit id=\"22\">\n                <source>This value should not be blank.</source>\n                <target>Cette valeur ne doit pas être vide.</target>\n            </trans-unit>\n            <trans-unit id=\"23\">\n                <source>This value should not be null.</source>\n                <target>Cette valeur ne doit pas être nulle.</target>\n            </trans-unit>\n            <trans-unit id=\"24\">\n                <source>This value should be null.</source>\n                <target>Cette valeur doit être nulle.</target>\n            </trans-unit>\n            <trans-unit id=\"25\">\n                <source>This value is not valid.</source>\n                <target>Cette valeur n'est pas valide.</target>\n            </trans-unit>\n            <trans-unit id=\"26\">\n                <source>This value is not a valid time.</source>\n                <target>Cette valeur n'est pas une heure valide.</target>\n            </trans-unit>\n            <trans-unit id=\"27\">\n                <source>This value is not a valid URL.</source>\n                <target>Cette valeur n'est pas une URL valide.</target>\n            </trans-unit>\n            <trans-unit id=\"31\">\n                <source>The two values should be equal.</source>\n                <target>Les deux valeurs doivent être identiques.</target>\n            </trans-unit>\n            <trans-unit id=\"32\">\n                <source>The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.</source>\n                <target>Le fichier est trop volumineux. Sa taille ne doit pas dépasser {{ limit }} {{ suffix }}.</target>\n            </trans-unit>\n            <trans-unit id=\"33\">\n                <source>The file is too large.</source>\n                <target>Le fichier est trop volumineux.</target>\n            </trans-unit>\n            <trans-unit id=\"34\">\n                <source>The file could not be uploaded.</source>\n                <target>Le téléchargement de ce fichier est impossible.</target>\n            </trans-unit>\n            <trans-unit id=\"35\">\n                <source>This value should be a valid number.</source>\n                <target>Cette valeur doit être un nombre.</target>\n            </trans-unit>\n            <trans-unit id=\"36\">\n                <source>This file is not a valid image.</source>\n                <target>Ce fichier n'est pas une image valide.</target>\n            </trans-unit>\n            <trans-unit id=\"37\" resname=\"This is not a valid IP address.\">\n                <source>This value is not a valid IP address.</source>\n                <target>Cette valeur n'est pas une adresse IP valide.</target>\n            </trans-unit>\n            <trans-unit id=\"38\">\n                <source>This value is not a valid language.</source>\n                <target>Cette langue n'est pas valide.</target>\n            </trans-unit>\n            <trans-unit id=\"39\">\n                <source>This value is not a valid locale.</source>\n                <target>Ce paramètre régional n'est pas valide.</target>\n            </trans-unit>\n            <trans-unit id=\"40\">\n                <source>This value is not a valid country.</source>\n                <target>Ce pays n'est pas valide.</target>\n            </trans-unit>\n            <trans-unit id=\"41\">\n                <source>This value is already used.</source>\n                <target>Cette valeur est déjà utilisée.</target>\n            </trans-unit>\n            <trans-unit id=\"42\">\n                <source>The size of the image could not be detected.</source>\n                <target>La taille de l'image n'a pas pu être détectée.</target>\n            </trans-unit>\n            <trans-unit id=\"43\">\n                <source>The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.</source>\n                <target>La largeur de l'image est trop grande ({{ width }}px). La largeur maximale autorisée est de {{ max_width }}px.</target>\n            </trans-unit>\n            <trans-unit id=\"44\">\n                <source>The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.</source>\n                <target>La largeur de l'image est trop petite ({{ width }}px). La largeur minimale attendue est de {{ min_width }}px.</target>\n            </trans-unit>\n            <trans-unit id=\"45\">\n                <source>The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.</source>\n                <target>La hauteur de l'image est trop grande ({{ height }}px). La hauteur maximale autorisée est de {{ max_height }}px.</target>\n            </trans-unit>\n            <trans-unit id=\"46\">\n                <source>The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.</source>\n                <target>La hauteur de l'image est trop petite ({{ height }}px). La hauteur minimale attendue est de {{ min_height }}px.</target>\n            </trans-unit>\n            <trans-unit id=\"47\">\n                <source>This value should be the user's current password.</source>\n                <target>Cette valeur doit être le mot de passe actuel de l'utilisateur.</target>\n            </trans-unit>\n            <trans-unit id=\"48\">\n                <source>This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.</source>\n                <target>Cette chaîne doit avoir exactement {{ limit }} caractère.|Cette chaîne doit avoir exactement {{ limit }} caractères.</target>\n            </trans-unit>\n            <trans-unit id=\"49\">\n                <source>The file was only partially uploaded.</source>\n                <target>Le fichier a été partiellement transféré.</target>\n            </trans-unit>\n            <trans-unit id=\"50\">\n                <source>No file was uploaded.</source>\n                <target>Aucun fichier n'a été transféré.</target>\n            </trans-unit>\n            <trans-unit id=\"51\" resname=\"No temporary folder was configured in php.ini.\">\n                <source>No temporary folder was configured in php.ini, or the configured folder does not exist.</source>\n                <target>Aucun répertoire temporaire n'a été configuré dans le php.ini, ou le répertoire configuré n'existe pas.</target>\n            </trans-unit>\n            <trans-unit id=\"52\">\n                <source>Cannot write temporary file to disk.</source>\n                <target>Impossible d'écrire le fichier temporaire sur le disque.</target>\n            </trans-unit>\n            <trans-unit id=\"53\">\n                <source>A PHP extension caused the upload to fail.</source>\n                <target>Une extension PHP a empêché le transfert du fichier.</target>\n            </trans-unit>\n            <trans-unit id=\"54\">\n                <source>This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.</source>\n                <target>Cette collection doit contenir {{ limit }} élément ou plus.|Cette collection doit contenir {{ limit }} éléments ou plus.</target>\n            </trans-unit>\n            <trans-unit id=\"55\">\n                <source>This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.</source>\n                <target>Cette collection doit contenir {{ limit }} élément ou moins.|Cette collection doit contenir {{ limit }} éléments ou moins.</target>\n            </trans-unit>\n            <trans-unit id=\"56\">\n                <source>This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.</source>\n                <target>Cette collection doit contenir exactement {{ limit }} élément.|Cette collection doit contenir exactement {{ limit }} éléments.</target>\n            </trans-unit>\n            <trans-unit id=\"57\">\n                <source>Invalid card number.</source>\n                <target>Numéro de carte invalide.</target>\n            </trans-unit>\n            <trans-unit id=\"58\">\n                <source>Unsupported card type or invalid card number.</source>\n                <target>Type de carte non supporté ou numéro invalide.</target>\n            </trans-unit>\n            <trans-unit id=\"59\" resname=\"This is not a valid International Bank Account Number (IBAN).\">\n                <source>This value is not a valid International Bank Account Number (IBAN).</source>\n                <target>Cette valeur n'est pas un Numéro de Compte Bancaire International (IBAN) valide.</target>\n            </trans-unit>\n            <trans-unit id=\"60\">\n                <source>This value is not a valid ISBN-10.</source>\n                <target>Cette valeur n'est pas un code ISBN-10 valide.</target>\n            </trans-unit>\n            <trans-unit id=\"61\">\n                <source>This value is not a valid ISBN-13.</source>\n                <target>Cette valeur n'est pas un code ISBN-13 valide.</target>\n            </trans-unit>\n            <trans-unit id=\"62\">\n                <source>This value is neither a valid ISBN-10 nor a valid ISBN-13.</source>\n                <target>Cette valeur n'est ni un code ISBN-10, ni un code ISBN-13 valide.</target>\n            </trans-unit>\n            <trans-unit id=\"63\">\n                <source>This value is not a valid ISSN.</source>\n                <target>Cette valeur n'est pas un code ISSN valide.</target>\n            </trans-unit>\n            <trans-unit id=\"64\">\n                <source>This value is not a valid currency.</source>\n                <target>Cette valeur n'est pas une devise valide.</target>\n            </trans-unit>\n            <trans-unit id=\"65\">\n                <source>This value should be equal to {{ compared_value }}.</source>\n                <target>Cette valeur doit être égale à {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"66\">\n                <source>This value should be greater than {{ compared_value }}.</source>\n                <target>Cette valeur doit être supérieure à {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"67\">\n                <source>This value should be greater than or equal to {{ compared_value }}.</source>\n                <target>Cette valeur doit être supérieure ou égale à {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"68\">\n                <source>This value should be identical to {{ compared_value_type }} {{ compared_value }}.</source>\n                <target>Cette valeur doit être identique à {{ compared_value_type }} {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"69\">\n                <source>This value should be less than {{ compared_value }}.</source>\n                <target>Cette valeur doit être inférieure à {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"70\">\n                <source>This value should be less than or equal to {{ compared_value }}.</source>\n                <target>Cette valeur doit être inférieure ou égale à {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"71\">\n                <source>This value should not be equal to {{ compared_value }}.</source>\n                <target>Cette valeur ne doit pas être égale à {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"72\">\n                <source>This value should not be identical to {{ compared_value_type }} {{ compared_value }}.</source>\n                <target>Cette valeur ne doit pas être identique à {{ compared_value_type }} {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"73\">\n                <source>The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.</source>\n                <target>Le rapport largeur/hauteur de l'image est trop grand ({{ ratio }}). Le rapport maximal autorisé est {{ max_ratio }}.</target>\n            </trans-unit>\n            <trans-unit id=\"74\">\n                <source>The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.</source>\n                <target>Le rapport largeur/hauteur de l'image est trop petit ({{ ratio }}). Le rapport minimal attendu est {{ min_ratio }}.</target>\n            </trans-unit>\n            <trans-unit id=\"75\">\n                <source>The image is square ({{ width }}x{{ height }}px). Square images are not allowed.</source>\n                <target>L'image est carrée ({{ width }}x{{ height }}px). Les images carrées ne sont pas autorisées.</target>\n            </trans-unit>\n            <trans-unit id=\"76\">\n                <source>The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.</source>\n                <target>L'image est au format paysage ({{ width }}x{{ height }}px). Les images au format paysage ne sont pas autorisées.</target>\n            </trans-unit>\n            <trans-unit id=\"77\">\n                <source>The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.</source>\n                <target>L'image est au format portrait ({{ width }}x{{ height }}px). Les images au format portrait ne sont pas autorisées.</target>\n            </trans-unit>\n            <trans-unit id=\"78\">\n                <source>An empty file is not allowed.</source>\n                <target>Un fichier vide n'est pas autorisé.</target>\n            </trans-unit>\n            <trans-unit id=\"79\">\n                <source>The host could not be resolved.</source>\n                <target>Le nom de domaine n'a pas pu être résolu.</target>\n            </trans-unit>\n            <trans-unit id=\"80\">\n                <source>This value does not match the expected {{ charset }} charset.</source>\n                <target>Cette valeur ne correspond pas au jeu de caractères {{ charset }} attendu.</target>\n            </trans-unit>\n            <trans-unit id=\"81\" resname=\"This is not a valid Business Identifier Code (BIC).\">\n                <source>This value is not a valid Business Identifier Code (BIC).</source>\n                <target>Cette valeur n'est pas un Code Identifiant de Business (BIC) valide.</target>\n            </trans-unit>\n            <trans-unit id=\"82\">\n                <source>Error</source>\n                <target>Erreur</target>\n            </trans-unit>\n            <trans-unit id=\"83\" resname=\"This is not a valid UUID.\">\n                <source>This value is not a valid UUID.</source>\n                <target>Cette valeur n'est pas un UUID valide.</target>\n            </trans-unit>\n            <trans-unit id=\"84\">\n                <source>This value should be a multiple of {{ compared_value }}.</source>\n                <target>Cette valeur doit être un multiple de {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"85\">\n                <source>This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.</source>\n                <target>Ce code d'identification d'entreprise (BIC) n'est pas associé à l'IBAN {{ iban }}.</target>\n            </trans-unit>\n            <trans-unit id=\"86\">\n                <source>This value should be valid JSON.</source>\n                <target>Cette valeur doit être un JSON valide.</target>\n            </trans-unit>\n            <trans-unit id=\"87\">\n                <source>This collection should contain only unique elements.</source>\n                <target>Cette collection ne doit pas comporter de doublons.</target>\n            </trans-unit>\n            <trans-unit id=\"88\">\n                <source>This value should be positive.</source>\n                <target>Cette valeur doit être strictement positive.</target>\n            </trans-unit>\n            <trans-unit id=\"89\">\n                <source>This value should be either positive or zero.</source>\n                <target>Cette valeur doit être supérieure ou égale à zéro.</target>\n            </trans-unit>\n            <trans-unit id=\"90\">\n                <source>This value should be negative.</source>\n                <target>Cette valeur doit être strictement négative.</target>\n            </trans-unit>\n            <trans-unit id=\"91\">\n                <source>This value should be either negative or zero.</source>\n                <target>Cette valeur doit être inférieure ou égale à zéro.</target>\n            </trans-unit>\n            <trans-unit id=\"92\">\n                <source>This value is not a valid timezone.</source>\n                <target>Cette valeur n'est pas un fuseau horaire valide.</target>\n            </trans-unit>\n            <trans-unit id=\"93\">\n                <source>This password has been leaked in a data breach, it must not be used. Please use another password.</source>\n                <target>Ce mot de passe a été divulgué lors d'une fuite de données, il ne doit plus être utilisé. Veuillez utiliser un autre mot de passe.</target>\n            </trans-unit>\n            <trans-unit id=\"94\">\n                <source>This value should be between {{ min }} and {{ max }}.</source>\n                <target>Cette valeur doit être comprise entre {{ min }} et {{ max }}.</target>\n            </trans-unit>\n            <trans-unit id=\"95\">\n                <source>This value is not a valid hostname.</source>\n                <target>Cette valeur n'est pas un nom d'hôte valide.</target>\n            </trans-unit>\n            <trans-unit id=\"96\">\n                <source>The number of elements in this collection should be a multiple of {{ compared_value }}.</source>\n                <target>Le nombre d'éléments de cette collection doit être un multiple de {{ compared_value }}.</target>\n            </trans-unit>\n            <trans-unit id=\"97\">\n                <source>This value should satisfy at least one of the following constraints:</source>\n                <target>Cette valeur doit satisfaire à au moins une des contraintes suivantes :</target>\n            </trans-unit>\n            <trans-unit id=\"98\">\n                <source>Each element of this collection should satisfy its own set of constraints.</source>\n                <target>Chaque élément de cette collection doit satisfaire à son propre jeu de contraintes.</target>\n            </trans-unit>\n            <trans-unit id=\"99\">\n                <source>This value is not a valid International Securities Identification Number (ISIN).</source>\n                <target>Cette valeur n'est pas un code international de sécurité valide (ISIN).</target>\n            </trans-unit>\n            <trans-unit id=\"100\">\n                <source>This value should be a valid expression.</source>\n                <target>Cette valeur doit être une expression valide.</target>\n            </trans-unit>\n            <trans-unit id=\"101\">\n                <source>This value is not a valid CSS color.</source>\n                <target>Cette valeur n'est pas une couleur CSS valide.</target>\n            </trans-unit>\n            <trans-unit id=\"102\">\n                <source>This value is not a valid CIDR notation.</source>\n                <target>Cette valeur n'est pas une notation CIDR valide.</target>\n            </trans-unit>\n            <trans-unit id=\"103\">\n                <source>The value of the netmask should be between {{ min }} and {{ max }}.</source>\n                <target>La valeur du masque de réseau doit être comprise entre {{ min }} et {{ max }}.</target>\n            </trans-unit>\n            <trans-unit id=\"104\">\n                <source>The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less.</source>\n                <target>Le nom du fichier est trop long. Il doit contenir au maximum {{ filename_max_length }} caractère.|Le nom de fichier est trop long. Il doit contenir au maximum {{ filename_max_length }} caractères.</target>\n            </trans-unit>\n            <trans-unit id=\"105\">\n                <source>The password strength is too low. Please use a stronger password.</source>\n                <target>La force du mot de passe est trop faible. Veuillez utiliser un mot de passe plus fort.</target>\n            </trans-unit>\n            <trans-unit id=\"106\">\n                <source>This value contains characters that are not allowed by the current restriction-level.</source>\n                <target>Cette valeur contient des caractères qui ne sont pas autorisés par le niveau de restriction actuel.</target>\n            </trans-unit>\n            <trans-unit id=\"107\">\n                <source>Using invisible characters is not allowed.</source>\n                <target>Utiliser des caractères invisibles n'est pas autorisé.</target>\n            </trans-unit>\n            <trans-unit id=\"108\">\n                <source>Mixing numbers from different scripts is not allowed.</source>\n                <target>Mélanger des chiffres provenant de différents scripts n'est pas autorisé.</target>\n            </trans-unit>\n            <trans-unit id=\"109\">\n                <source>Using hidden overlay characters is not allowed.</source>\n                <target>Utiliser des caractères de superposition cachés n'est pas autorisé.</target>\n            </trans-unit>\n            <trans-unit id=\"110\">\n                <source>The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}.</source>\n                <target>L'extension du fichier est invalide ({{ extension }}). Les extensions autorisées sont {{ extensions }}.</target>\n            </trans-unit>\n            <trans-unit id=\"111\">\n                <source>The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}.</source>\n                <target>L'encodage de caractères détecté est invalide ({{ detected }}). Les encodages autorisés sont {{ encodings }}.</target>\n            </trans-unit>\n            <trans-unit id=\"112\">\n                <source>This value is not a valid MAC address.</source>\n                <target>Cette valeur n'est pas une adresse MAC valide.</target>\n            </trans-unit>\n            <trans-unit id=\"113\">\n                <source>This URL is missing a top-level domain.</source>\n                <target>Cette URL doit contenir un domaine de premier niveau.</target>\n            </trans-unit>\n            <trans-unit id=\"114\">\n                <source>This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.</source>\n                <target>Cette valeur est trop courte. Elle doit contenir au moins un mot.|Cette valeur est trop courte. Elle doit contenir au moins {{ min }} mots.</target>\n            </trans-unit>\n            <trans-unit id=\"115\">\n                <source>This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.</source>\n                <target>Cette valeur est trop longue. Elle doit contenir au maximum un mot.|Cette valeur est trop longue. Elle doit contenir au maximum {{ max }} mots.</target>\n            </trans-unit>\n            <trans-unit id=\"116\">\n                <source>This value does not represent a valid week in the ISO 8601 format.</source>\n                <target>Cette valeur ne représente pas une semaine valide au format ISO 8601.</target>\n            </trans-unit>\n            <trans-unit id=\"117\">\n                <source>This value is not a valid week.</source>\n                <target>Cette valeur n'est pas une semaine valide.</target>\n            </trans-unit>\n            <trans-unit id=\"118\">\n                <source>This value should not be before week \"{{ min }}\".</source>\n                <target>Cette valeur ne doit pas être antérieure à la semaine \"{{ min }}\".</target>\n            </trans-unit>\n            <trans-unit id=\"119\">\n                <source>This value should not be after week \"{{ max }}\".</source>\n                <target>Cette valeur ne doit pas être postérieure à la semaine \"{{ max }}\".</target>\n            </trans-unit>\n            <trans-unit id=\"121\">\n                <source>This value is not a valid Twig template.</source>\n                <target>Cette valeur n'est pas un modèle Twig valide.</target>\n            </trans-unit>\n            <trans-unit id=\"122\">\n                <source>This file is not a valid video.</source>\n                <target>Ce fichier n’est pas une vidéo valide.</target>\n            </trans-unit>\n            <trans-unit id=\"123\">\n                <source>The size of the video could not be detected.</source>\n                <target>La taille de la vidéo n’a pas pu être détectée.</target>\n            </trans-unit>\n            <trans-unit id=\"124\">\n                <source>The video width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.</source>\n                <target>La largeur de la vidéo est trop grande ({{ width }}px). La largeur maximale autorisée est de {{ max_width }}px.</target>\n            </trans-unit>\n            <trans-unit id=\"125\">\n                <source>The video width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.</source>\n                <target>La largeur de la vidéo est trop petite ({{ width }}px). La largeur minimale attendue est de {{ min_width }}px.</target>\n            </trans-unit>\n            <trans-unit id=\"126\">\n                <source>The video height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.</source>\n                <target>La hauteur de la vidéo est trop grande ({{ height }}px). La hauteur maximale autorisée est de {{ max_height }}px.</target>\n            </trans-unit>\n            <trans-unit id=\"127\">\n                <source>The video height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.</source>\n                <target>La hauteur de la vidéo est trop petite ({{ height }}px). La hauteur minimale attendue est de {{ min_height }}px.</target>\n            </trans-unit>\n            <trans-unit id=\"128\">\n                <source>The video has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.</source>\n                <target>La vidéo a trop peu de pixels ({{ pixels }}). La quantité minimale attendue est de {{ min_pixels }} pixels.</target>\n            </trans-unit>\n            <trans-unit id=\"129\">\n                <source>The video has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.</source>\n                <target>La vidéo contient trop de pixels ({{ pixels }}). La quantité maximale attendue est de {{ max_pixels }} pixels.</target>\n            </trans-unit>\n            <trans-unit id=\"130\">\n                <source>The video ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.</source>\n                <target>Le ratio de la vidéo est trop élevé ({{ ratio }}). Le ratio maximal autorisé est de {{ max_ratio }}.</target>\n            </trans-unit>\n            <trans-unit id=\"131\">\n                <source>The video ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.</source>\n                <target>Le ratio de la vidéo est trop petit ({{ ratio }}). Le ratio minimum attendu est de {{ min_ratio }}.</target>\n            </trans-unit>\n            <trans-unit id=\"132\">\n                <source>The video is square ({{ width }}x{{ height }}px). Square videos are not allowed.</source>\n                <target>La vidéo est carrée ({{ width }}x{{ height }}px). Les vidéos carrées ne sont pas autorisées.</target>\n            </trans-unit>\n            <trans-unit id=\"133\">\n                <source>The video is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented videos are not allowed.</source>\n                <target>La vidéo est au format paysage ({{ width }}x{{ height }} px). Les vidéos au format paysage ne sont pas autorisées.</target>\n            </trans-unit>\n            <trans-unit id=\"134\">\n                <source>The video is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented videos are not allowed.</source>\n                <target>La vidéo est orientée en portrait ({{ width }}x{{ height }} px). Les vidéos en orientation portrait ne sont pas autorisées.</target>\n            </trans-unit>\n            <trans-unit id=\"135\">\n                <source>The video file is corrupted.</source>\n                <target>Le fichier vidéo est corrompu.</target>\n            </trans-unit>\n            <trans-unit id=\"136\">\n                <source>The video contains multiple streams. Only one stream is allowed.</source>\n                <target>La vidéo contient plusieurs flux. Un seul flux est autorisé.</target>\n            </trans-unit>\n            <trans-unit id=\"137\">\n                <source>Unsupported video codec \"{{ codec }}\".</source>\n                <target>Le codec vidéo «{{ codec }}» est non pris en charge.</target>\n            </trans-unit>\n            <trans-unit id=\"138\">\n                <source>Unsupported video container \"{{ container }}\".</source>\n                <target>Le conteneur vidéo «{{ container }}» est non pris en charge.</target>\n            </trans-unit>\n            <trans-unit id=\"139\">\n                <source>The image file is corrupted.</source>\n                <target>Le fichier image est corrompu.</target>\n            </trans-unit>\n            <trans-unit id=\"140\">\n                <source>The image has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.</source>\n                <target>L’image comporte trop peu de pixels ({{ pixels }}). La quantité minimale attendue est de {{ min_pixels }} pixels.</target>\n            </trans-unit>\n            <trans-unit id=\"141\">\n                <source>The image has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.</source>\n                <target>L’image contient trop de pixels ({{ pixels }}). La quantité maximale attendue est de {{ max_pixels }} pixels.</target>\n            </trans-unit>\n            <trans-unit id=\"142\">\n                <source>This filename does not match the expected charset.</source>\n                <target>Le nom de fichier ne correspond pas au jeu de caractères attendu.</target>\n            </trans-unit>\n            <trans-unit id=\"9gDpv.9\" resname=\"form.uri.unique\">\n                <source>form.uri.unique</source>\n                <target>Cette URI est déjà utilisée avec ce principal. Veuillez en choisir une autre.</target>\n            </trans-unit>\n        </body>\n    </file>\n</xliff>"
  }
]