Browse Source

Merge branch 'master' into storage-id-cache-memcache

John Molakvoæ 1 month ago
parent
commit
8c568e455a
100 changed files with 1404 additions and 291 deletions
  1. 1 41
      .drone.yml
  2. 5 5
      .github/CODEOWNERS
  3. 2 2
      .github/workflows/cypress.yml
  4. 1 1
      .github/workflows/files-external-ftp.yml
  5. 2 2
      .github/workflows/files-external-s3.yml
  6. 1 1
      .github/workflows/files-external-sftp.yml
  7. 108 0
      .github/workflows/files-external-smb.yml
  8. 1 1
      .github/workflows/files-external-webdav.yml
  9. 93 0
      .github/workflows/files-external.yml
  10. 1 1
      .github/workflows/node-tests.yml
  11. 1 1
      .github/workflows/npm-audit-fix.yml
  12. 1 1
      .github/workflows/object-storage-azure.yml
  13. 1 1
      .github/workflows/object-storage-s3.yml
  14. 1 1
      .github/workflows/object-storage-swift.yml
  15. 1 1
      .github/workflows/phpunit-mariadb.yml
  16. 1 1
      .github/workflows/phpunit-memcached.yml
  17. 1 1
      .github/workflows/phpunit-mysql.yml
  18. 1 1
      .github/workflows/phpunit-nodb.yml
  19. 1 1
      .github/workflows/phpunit-oci.yml
  20. 1 1
      .github/workflows/phpunit-pgsql.yml
  21. 1 1
      .github/workflows/phpunit-sqlite.yml
  22. 1 1
      .github/workflows/update-cacert-bundle.yml
  23. 1 1
      .github/workflows/update-code-signing-crl.yml
  24. 1 1
      .github/workflows/update-psalm-baseline.yml
  25. 1 1
      apps/admin_audit/l10n/he.js
  26. 1 1
      apps/admin_audit/l10n/he.json
  27. 8 0
      apps/cloud_federation_api/l10n/es_MX.js
  28. 6 0
      apps/cloud_federation_api/l10n/es_MX.json
  29. 17 0
      apps/comments/l10n/es_MX.js
  30. 17 0
      apps/comments/l10n/es_MX.json
  31. 1 0
      apps/comments/l10n/fr.js
  32. 1 0
      apps/comments/l10n/fr.json
  33. 2 2
      apps/comments/l10n/he.js
  34. 2 2
      apps/comments/l10n/he.json
  35. 1 0
      apps/comments/l10n/ko.js
  36. 1 0
      apps/comments/l10n/ko.json
  37. 2 0
      apps/comments/l10n/pt_BR.js
  38. 2 0
      apps/comments/l10n/pt_BR.json
  39. 6 0
      apps/comments/l10n/sk.js
  40. 6 0
      apps/comments/l10n/sk.json
  41. 16 9
      apps/comments/src/services/DavClient.js
  42. 4 6
      apps/comments/src/services/GetComments.ts
  43. 2 0
      apps/contactsinteraction/l10n/ast.js
  44. 2 0
      apps/contactsinteraction/l10n/ast.json
  45. 2 0
      apps/contactsinteraction/l10n/fr.js
  46. 2 0
      apps/contactsinteraction/l10n/fr.json
  47. 1 1
      apps/contactsinteraction/l10n/he.js
  48. 1 1
      apps/contactsinteraction/l10n/he.json
  49. 2 0
      apps/contactsinteraction/l10n/pt_BR.js
  50. 2 0
      apps/contactsinteraction/l10n/pt_BR.json
  51. 1 1
      apps/dashboard/l10n/he.js
  52. 1 1
      apps/dashboard/l10n/he.json
  53. 1 0
      apps/dashboard/l10n/pt_BR.js
  54. 1 0
      apps/dashboard/l10n/pt_BR.json
  55. 3 14
      apps/dashboard/lib/Controller/DashboardApiController.php
  56. 6 26
      apps/dashboard/lib/Controller/DashboardController.php
  57. 2 8
      apps/dashboard/lib/Controller/LayoutApiController.php
  58. 1 0
      apps/dav/appinfo/info.xml
  59. 1 0
      apps/dav/composer/composer/autoload_classmap.php
  60. 1 0
      apps/dav/composer/composer/autoload_static.php
  61. 114 0
      apps/dav/l10n/ast.js
  62. 112 0
      apps/dav/l10n/ast.json
  63. 2 0
      apps/dav/l10n/de.js
  64. 2 0
      apps/dav/l10n/de.json
  65. 2 0
      apps/dav/l10n/pt_BR.js
  66. 2 0
      apps/dav/l10n/pt_BR.json
  67. 26 0
      apps/dav/l10n/sk.js
  68. 26 0
      apps/dav/l10n/sk.json
  69. 1 1
      apps/dav/l10n/uk.js
  70. 1 1
      apps/dav/l10n/uk.json
  71. 1 0
      apps/dav/l10n/zh_CN.js
  72. 1 0
      apps/dav/l10n/zh_CN.json
  73. 1 1
      apps/dav/lib/BulkUpload/BulkUploadPlugin.php
  74. 102 56
      apps/dav/lib/CalDAV/CalDavBackend.php
  75. 7 0
      apps/dav/lib/CalDAV/Schedule/Plugin.php
  76. 22 20
      apps/dav/lib/CardDAV/CardDavBackend.php
  77. 86 0
      apps/dav/lib/Command/FixCalendarSyncCommand.php
  78. 2 6
      apps/dav/lib/Connector/Sabre/FilesPlugin.php
  79. 3 3
      apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
  80. 1 0
      apps/dav/lib/Connector/Sabre/Principal.php
  81. 1 0
      apps/dav/lib/Connector/Sabre/ServerFactory.php
  82. 2 3
      apps/dav/lib/Controller/DirectController.php
  83. 135 4
      apps/dav/lib/DAV/CustomPropertiesBackend.php
  84. 6 3
      apps/dav/lib/Direct/DirectFile.php
  85. 2 1
      apps/dav/lib/RootCollection.php
  86. 1 0
      apps/dav/lib/Server.php
  87. 10 3
      apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
  88. 1 1
      apps/dav/lib/Upload/ChunkingV2Plugin.php
  89. 19 11
      apps/dav/src/dav/client.js
  90. 5 3
      apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php
  91. 89 0
      apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
  92. 38 0
      apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php
  93. 1 0
      apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php
  94. 6 6
      apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
  95. 4 4
      apps/dav/tests/unit/Controller/DirectControllerTest.php
  96. 195 5
      apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php
  97. 2 2
      apps/dav/tests/unit/Direct/DirectFileTest.php
  98. 16 16
      apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php
  99. 1 0
      apps/encryption/l10n/es_MX.js
  100. 1 0
      apps/encryption/l10n/es_MX.json

+ 1 - 41
.drone.yml

@@ -122,46 +122,6 @@ trigger:
     - pull_request
     - push
 
----
-kind: pipeline
-name: samba
-
-steps:
-- name: submodules
-  image: ghcr.io/nextcloud/continuous-integration-alpine-git:latest
-  commands:
-    - git submodule update --init
-- name: sqlite-php8.0-samba-native
-  image: ghcr.io/nextcloud/continuous-integration-samba-native-php8.0:latest
-  commands:
-    - smbd -D -FS &
-    - ./autotest-external.sh sqlite smb-linux
-    - wget https://codecov.io/bash -O codecov.sh
-    - sh -c "if [ '$DRONE_BUILD_EVENT' = 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -P $DRONE_PULL_REQUEST -t 117641e2-a9e8-4b7b-984b-ae872d9b05f5 -f tests/autotest-external-clover-sqlite.xml; fi"
-    - sh -c "if [ '$DRONE_BUILD_EVENT' != 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -t 117641e2-a9e8-4b7b-984b-ae872d9b05f5 -f tests/autotest-external-clover-sqlite.xml; fi"
-    - sh -c "if [ '$DRONE_BUILD_EVENT' = 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -P $DRONE_PULL_REQUEST -t 117641e2-a9e8-4b7b-984b-ae872d9b05f5 -f tests/autotest-external-clover-sqlite-smb-linux.xml; fi"
-    - sh -c "if [ '$DRONE_BUILD_EVENT' != 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -t 117641e2-a9e8-4b7b-984b-ae872d9b05f5 -f tests/autotest-external-clover-sqlite-smb-linux.xml; fi"
-# Temporarily disabled because it times out for unknown reasons 98% of the time
-#- name: sqlite-php8.0-samba-non-native
-#  image: ghcr.io/nextcloud/continuous-integration-samba-non-native-php8.0:latest
-#  commands:
-#    - smbd -D -FS &
-#    - ./autotest-external.sh sqlite smb-linux
-#    - wget https://codecov.io/bash -O codecov.sh
-#    - sh -c "if [ '$DRONE_BUILD_EVENT' = 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -P $DRONE_PULL_REQUEST -t 117641e2-a9e8-4b7b-984b-ae872d9b05f5 -f tests/autotest-external-clover-sqlite.xml; fi"
-#    - sh -c "if [ '$DRONE_BUILD_EVENT' != 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -t 117641e2-a9e8-4b7b-984b-ae872d9b05f5 -f tests/autotest-external-clover-sqlite.xml; fi"
-#    - sh -c "if [ '$DRONE_BUILD_EVENT' = 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -P $DRONE_PULL_REQUEST -t 117641e2-a9e8-4b7b-984b-ae872d9b05f5 -f tests/autotest-external-clover-sqlite-smb-linux.xml; fi"
-#    - sh -c "if [ '$DRONE_BUILD_EVENT' != 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -t 117641e2-a9e8-4b7b-984b-ae872d9b05f5 -f tests/autotest-external-clover-sqlite-smb-linux.xml; fi"
-
-trigger:
-  branch:
-    - master
-    - stable*
-  event:
-    - pull_request
-    - push
-
-
 ---
 kind: signature
-hmac: e34c8c2ca36355a8dcaf01c7bd14c53bd42d25a4d631533e2c8109e2690c2a98
+hmac: f1a7a4774aef02c37a06ec6189d0acfefd847b66661ac4f6aab243f12f979158

+ 5 - 5
.github/CODEOWNERS

@@ -29,11 +29,11 @@
 /apps/workflowengine/appinfo/info.xml         @blizzz @juliushaertl
 
 # Frontend expertise
-/apps/files/src                  @skjnldsv
-/apps/files_external/src         @skjnldsv
-/apps/files_reminders/src        @skjnldsv
-/apps/files_sharing/src/actions  @skjnldsv
-/apps/files_trashbin/src         @skjnldsv
+/apps/files/src*                  @skjnldsv
+/apps/files_external/src*         @skjnldsv
+/apps/files_reminders/src*        @skjnldsv
+/apps/files_sharing/src/actions*  @skjnldsv
+/apps/files_trashbin/src*         @skjnldsv
 
 # Security team
 /resources/codesigning              @mgallien @miaulalala @nickvergessen

+ 2 - 2
.github/workflows/cypress.yml

@@ -31,7 +31,7 @@ jobs:
 
       - name: Check composer.json
         id: check_composer
-        uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2
+        uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v2
         with:
           files: "composer.json"
 
@@ -101,7 +101,7 @@ jobs:
         run: npm i -g npm@"${{ needs.init.outputs.npmVersion }}"
 
       - name: Run ${{ matrix.containers == 'component' && 'component' || 'E2E' }} cypress tests
-        uses: cypress-io/github-action@ebe8b24c4428922d0f793a5c4c96853a633180e3 # v6.6.0
+        uses: cypress-io/github-action@1b70233146622b69e789ccdd4f9452adc638d25a # v6.6.1
         with:
           component: ${{ matrix.containers == 'component' }}
           group: ${{ matrix.use-cypress-cloud && matrix.containers == 'component' && 'Run component' || matrix.use-cypress-cloud && 'Run E2E' || '' }}

+ 1 - 1
.github/workflows/files-external-ftp.yml

@@ -98,7 +98,7 @@ jobs:
 
       - name: Upload code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.xml
           flags: phpunit-files-external-ftp

+ 2 - 2
.github/workflows/files-external-s3.yml

@@ -96,7 +96,7 @@ jobs:
 
       - name: Upload code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.xml
           flags: phpunit-files-external-s3
@@ -163,7 +163,7 @@ jobs:
 
       - name: Upload code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.xml
           flags: phpunit-files-external-s3

+ 1 - 1
.github/workflows/files-external-sftp.yml

@@ -87,7 +87,7 @@ jobs:
 
       - name: Upload code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.xml
           flags: phpunit-files-external-sftp

+ 108 - 0
.github/workflows/files-external-smb.yml

@@ -0,0 +1,108 @@
+name: PHPUnit files_external SMB
+on:
+  pull_request:
+  schedule:
+    - cron: "5 2 * * *"
+
+concurrency:
+  group: files-external-smb-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  changes:
+    runs-on: ubuntu-latest-low
+
+    outputs:
+      src: ${{ steps.changes.outputs.src}}
+
+    steps:
+      - uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0.0
+        id: changes
+        continue-on-error: true
+        with:
+          filters: |
+            src:
+              - '.github/workflows/**'
+              - '3rdparty/**'
+              - 'apps/files_external/**'
+              - 'vendor/**'
+              - 'vendor-bin/**'
+              - 'composer.json'
+              - 'composer.lock'
+              - '**.php'
+
+  files-external-smb:
+    runs-on: ubuntu-latest
+    needs: changes
+
+    if: ${{ github.repository_owner != 'nextcloud-gmbh' && needs.changes.outputs.src != 'false' }}
+
+    strategy:
+      matrix:
+        php-versions: ['8.0', '8.3']
+        include:
+          - php-versions: '8.0'
+            coverage: ${{ github.event_name != 'pull_request' }}
+
+    name: php${{ matrix.php-versions }}-smb
+
+    services:
+      samba:
+        image: ghcr.io/nextcloud/continuous-integration-samba:latest
+        ports:
+          - 445:445
+
+    steps:
+      - name: Checkout server
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
+        with:
+          submodules: true
+
+      - name: Set up php ${{ matrix.php-versions }}
+        uses: shivammathur/setup-php@4bd44f22a98a19e0950cbad5f31095157cc9621b # v2
+        with:
+          php-version: ${{ matrix.php-versions }}
+          # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation
+          extensions: ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, smbclient, sqlite, pdo_sqlite
+          coverage: ${{ matrix.coverage && 'xdebug' || 'none' }}
+          ini-file: development
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Set up smbclient
+        # This is needed as icewind/smb php library for notify
+        run: sudo apt-get install -y smbclient
+
+      - name: Set up Nextcloud
+        run: |
+          composer install
+          ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password
+          ./occ config:system:set --value true --type boolean allow_local_remote_servers
+          ./occ app:enable --force files_external
+          echo "<?php return ['run'=>true, 'host'=>'localhost', 'user'=>'test', 'password'=>'test', 'root'=>'', 'share'=>'public'];" > apps/files_external/tests/config.smb.php
+
+      - name: Wait for smb
+        run: |
+          apps/files_external/tests/env/wait-for-connection 127.0.0.1 445 60
+
+      - name: PHPUnit
+        run: composer run test:files_external -- --verbose \
+          apps/files_external/tests/Storage/SmbTest.php \
+          ${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
+
+      - name: Upload code coverage
+        if: ${{ !cancelled() && matrix.coverage }}
+        uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # v3.1.5
+        with:
+          files: ./clover.xml
+          flags: phpunit-files-external-smb
+
+  files-external-smb-summary:
+    runs-on: ubuntu-latest-low
+    needs: [changes, files-external-smb]
+
+    if: always()
+
+    steps:
+      - name: Summary status
+        run: if ${{ needs.changes.outputs.src != 'false' && needs.files-external-smb.result != 'success' }}; then exit 1; fi

+ 1 - 1
.github/workflows/files-external-webdav.yml

@@ -89,7 +89,7 @@ jobs:
 
       - name: Upload code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # v3.1.5
+        uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3.1.6
         with:
           files: ./clover.xml
           flags: phpunit-files-external-webdav

+ 93 - 0
.github/workflows/files-external.yml

@@ -0,0 +1,93 @@
+name: PHPUnit files_external generic
+on:
+  pull_request:
+  schedule:
+    - cron: "5 2 * * *"
+
+concurrency:
+  group: files-external-generic-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  changes:
+    runs-on: ubuntu-latest-low
+
+    outputs:
+      src: ${{ steps.changes.outputs.src}}
+
+    steps:
+      - uses: dorny/paths-filter@ebc4d7e9ebcb0b1eb21480bb8f43113e996ac77a # v3.0.1
+        id: changes
+        continue-on-error: true
+        with:
+          filters: |
+            src:
+              - '.github/workflows/**'
+              - '3rdparty/**'
+              - 'apps/files_external/**'
+              - 'vendor/**'
+              - 'vendor-bin/**'
+              - 'composer.json'
+              - 'composer.lock'
+
+  files-external-generic:
+    runs-on: ubuntu-latest
+    needs: changes
+
+    if: ${{ github.repository_owner != 'nextcloud-gmbh' && needs.changes.outputs.src != 'false' }}
+
+    strategy:
+      matrix:
+        php-versions: ['8.0', '8.1', '8.2', '8.3']
+        include:
+          - php-versions: '8.2'
+            coverage: ${{ github.event_name != 'pull_request' }}
+
+    name: php${{ matrix.php-versions }}-generic
+
+    steps:
+      - name: Checkout server
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
+        with:
+          submodules: true
+
+      - name: Set up php ${{ matrix.php-versions }}
+        uses: shivammathur/setup-php@4bd44f22a98a19e0950cbad5f31095157cc9621b # v2
+        with:
+          php-version: ${{ matrix.php-versions }}
+          # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation
+          extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, redis, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
+          coverage: ${{ matrix.coverage && 'xdebug' || 'none' }}
+          ini-file: development
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Set up Nextcloud
+        env:
+          OBJECT_STORE_KEY: nextcloud
+          OBJECT_STORE_SECRET: bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ=
+        run: |
+          composer install
+          ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password
+          ./occ app:enable --force files_external
+
+      - name: PHPUnit
+        run: composer run test:files_external \
+          ${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
+
+      - name: Upload code coverage
+        if: ${{ !cancelled() && matrix.coverage }}
+        uses: codecov/codecov-action@v4
+        with:
+          files: ./clover.xml
+          flags: phpunit-files-external-generic
+
+  files-external-summary:
+    runs-on: ubuntu-latest-low
+    needs: [changes, files-external-generic ]
+
+    if: always()
+
+    steps:
+      - name: Summary status
+        run: if ${{ needs.changes.outputs.src != 'false' && needs.files-external-generic.result != 'success' }}; then exit 1; fi

+ 1 - 1
.github/workflows/node-tests.yml

@@ -89,7 +89,7 @@ jobs:
         run: npm run test:coverage
 
       - name: Collect coverage
-        uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+        uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3.1.6
         with:
           files: ./coverage/lcov.info
 

+ 1 - 1
.github/workflows/npm-audit-fix.yml

@@ -58,7 +58,7 @@ jobs:
 
       - name: Create Pull Request
         if: always()
-        uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5
+        uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # v5
         with:
           token: ${{ secrets.COMMAND_BOT_PAT }}
           commit-message: "chore(deps): fix npm audit"

+ 1 - 1
.github/workflows/object-storage-azure.yml

@@ -103,7 +103,7 @@ jobs:
 
       - name: Upload code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.xml
           flags: phpunit-azure

+ 1 - 1
.github/workflows/object-storage-s3.yml

@@ -109,7 +109,7 @@ jobs:
 
       - name: Upload code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.xml
           flags: phpunit-s3

+ 1 - 1
.github/workflows/object-storage-swift.yml

@@ -99,7 +99,7 @@ jobs:
 
       - name: Upload code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.xml
           flags: phpunit-swift

+ 1 - 1
.github/workflows/phpunit-mariadb.yml

@@ -119,7 +119,7 @@ jobs:
 
       - name: Upload db code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.db.xml
           flags: phpunit-mariadb

+ 1 - 1
.github/workflows/phpunit-memcached.yml

@@ -98,7 +98,7 @@ jobs:
 
       - name: Upload code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.xml
           flags: phpunit-memcached

+ 1 - 1
.github/workflows/phpunit-mysql.yml

@@ -119,7 +119,7 @@ jobs:
 
       - name: Upload db code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.db.xml
           flags: phpunit-mysql

+ 1 - 1
.github/workflows/phpunit-nodb.yml

@@ -102,7 +102,7 @@ jobs:
 
       - name: Upload nodb code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.nodb.xml
           flags: phpunit-nodb

+ 1 - 1
.github/workflows/phpunit-oci.yml

@@ -117,7 +117,7 @@ jobs:
 
       - name: Upload db code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.db.xml
           flags: phpunit-oci

+ 1 - 1
.github/workflows/phpunit-pgsql.yml

@@ -114,7 +114,7 @@ jobs:
 
       - name: Upload db code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.db.xml
           flags: phpunit-postgres

+ 1 - 1
.github/workflows/phpunit-sqlite.yml

@@ -102,7 +102,7 @@ jobs:
 
       - name: Upload db code coverage
         if: ${{ !cancelled() && matrix.coverage }}
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
         with:
           files: ./clover.db.xml
           flags: phpunit-sqlite

+ 1 - 1
.github/workflows/update-cacert-bundle.yml

@@ -26,7 +26,7 @@ jobs:
         run: curl --etag-compare build/ca-bundle-etag.txt --etag-save build/ca-bundle-etag.txt --output resources/config/ca-bundle.crt https://curl.se/ca/cacert.pem
 
       - name: Create Pull Request
-        uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
+        uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc
         with:
           token: ${{ secrets.COMMAND_BOT_PAT }}
           commit-message: "fix(security): Update CA certificate bundle"

+ 1 - 1
.github/workflows/update-code-signing-crl.yml

@@ -29,7 +29,7 @@ jobs:
         run: openssl crl -verify -in resources/codesigning/root.crl -CAfile resources/codesigning/root.crt -noout
 
       - name: Create Pull Request
-        uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
+        uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc
         with:
           token: ${{ secrets.COMMAND_BOT_PAT }}
           commit-message: "fix(security): Update code signing revocation list"

+ 1 - 1
.github/workflows/update-psalm-baseline.yml

@@ -50,7 +50,7 @@ jobs:
           git checkout composer.json composer.lock lib/composer
 
       - name: Create Pull Request
-        uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
+        uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc
         with:
           token: ${{ secrets.COMMAND_BOT_PAT }}
           commit-message: Update psalm baseline

+ 1 - 1
apps/admin_audit/l10n/he.js

@@ -4,4 +4,4 @@ OC.L10N.register(
     "Auditing / Logging" : "פיקוח / תיעוד",
     "Provides logging abilities for Nextcloud such as logging file accesses or otherwise sensitive actions." : "מספק יכולות תיעוד ל־Nextcloud כגון תיעוד גישה ליומן התיעוד או פעולות רגישות אחרות."
 },
-"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;");
+"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;");

+ 1 - 1
apps/admin_audit/l10n/he.json

@@ -1,5 +1,5 @@
 { "translations": {
     "Auditing / Logging" : "פיקוח / תיעוד",
     "Provides logging abilities for Nextcloud such as logging file accesses or otherwise sensitive actions." : "מספק יכולות תיעוד ל־Nextcloud כגון תיעוד גישה ליומן התיעוד או פעולות רגישות אחרות."
-},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;"
+},"pluralForm" :"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;"
 }

+ 8 - 0
apps/cloud_federation_api/l10n/es_MX.js

@@ -0,0 +1,8 @@
+OC.L10N.register(
+    "cloud_federation_api",
+    {
+    "Cloud Federation API" : "API de la federación en la nube",
+    "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre sí e intercambien datos",
+    "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de la federación en la nube permite que varias instancias de Nextcloud se comuniquen entre sí e intercambien datos."
+},
+"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");

+ 6 - 0
apps/cloud_federation_api/l10n/es_MX.json

@@ -0,0 +1,6 @@
+{ "translations": {
+    "Cloud Federation API" : "API de la federación en la nube",
+    "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre sí e intercambien datos",
+    "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de la federación en la nube permite que varias instancias de Nextcloud se comuniquen entre sí e intercambien datos."
+},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
+}

+ 17 - 0
apps/comments/l10n/es_MX.js

@@ -9,12 +9,29 @@ OC.L10N.register(
     "%1$s commented on %2$s" : "%1$s comentó en %2$s",
     "{author} commented on {file}" : "{author} comentó en {file}",
     "<strong>Comments</strong> for files" : "<strong>Comentarios</strong> de los archivos",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Fue mencionado en \"{file}\", en un comentario realizado por un usuario que ha sido eliminado",
+    "{user} mentioned you in a comment on \"{file}\"" : "{user} lo mencionó en un comentario en \"{file}\"",
     "Files app plugin to add comments to files" : "Un complemento a la aplicación de Archivos para agregar comentarios a los archivos",
     "Edit comment" : "Editar comentario",
     "Delete comment" : "Borrar comentario",
+    "Cancel edit" : "Cancelar edición",
+    "New comment" : "Nuevo comentario",
+    "Write a comment …" : "Escribir un comentario …",
+    "Post comment" : "Publicar comentario",
+    "@ for mentions, : for emoji, / for smart picker" : "@ para menciones, : para emoticonos, / para selector inteligente",
+    "Could not reload comments" : "No se pudieron recargar los comentarios",
     "No comments yet, start the conversation!" : "¡Aún no hay comentarios, inicia la conversación!",
+    "No more messages" : "No hay más mensajes",
     "Retry" : "Reintentar",
+    "Failed to mark comments as read" : "No se pudieron marcar los comentarios como leídos",
+    "Unable to load the comments list" : "No se puede cargar la lista de comentarios",
+    "_1 new comment_::_{unread} new comments_" : ["1 comentario nuevo","{unread} nuevos comentarios","{unread} nuevos comentarios"],
     "Comment" : "Comentario",
+    "An error occurred while trying to edit the comment" : "Ocurrió un error al intentar editar el comentario",
+    "Comment deleted" : "Comentario borrado",
+    "An error occurred while trying to delete the comment" : "Ocurrió un error intentando borrar el comentario",
+    "An error occurred while trying to create the comment" : "Ocurrió un error al intentar crear el comentario",
+    "You were mentioned on \"{file}\", in a comment by a user that has since been deleted" : "Fue mencionado en \"{file}\", en un comentario realizado por un usuario que ha sido eliminado",
     "_%n unread comment_::_%n unread comments_" : ["%n comentarios sin leer","%n comentarios sin leer","%n comentarios sin leer"]
 },
 "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");

+ 17 - 0
apps/comments/l10n/es_MX.json

@@ -7,12 +7,29 @@
     "%1$s commented on %2$s" : "%1$s comentó en %2$s",
     "{author} commented on {file}" : "{author} comentó en {file}",
     "<strong>Comments</strong> for files" : "<strong>Comentarios</strong> de los archivos",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Fue mencionado en \"{file}\", en un comentario realizado por un usuario que ha sido eliminado",
+    "{user} mentioned you in a comment on \"{file}\"" : "{user} lo mencionó en un comentario en \"{file}\"",
     "Files app plugin to add comments to files" : "Un complemento a la aplicación de Archivos para agregar comentarios a los archivos",
     "Edit comment" : "Editar comentario",
     "Delete comment" : "Borrar comentario",
+    "Cancel edit" : "Cancelar edición",
+    "New comment" : "Nuevo comentario",
+    "Write a comment …" : "Escribir un comentario …",
+    "Post comment" : "Publicar comentario",
+    "@ for mentions, : for emoji, / for smart picker" : "@ para menciones, : para emoticonos, / para selector inteligente",
+    "Could not reload comments" : "No se pudieron recargar los comentarios",
     "No comments yet, start the conversation!" : "¡Aún no hay comentarios, inicia la conversación!",
+    "No more messages" : "No hay más mensajes",
     "Retry" : "Reintentar",
+    "Failed to mark comments as read" : "No se pudieron marcar los comentarios como leídos",
+    "Unable to load the comments list" : "No se puede cargar la lista de comentarios",
+    "_1 new comment_::_{unread} new comments_" : ["1 comentario nuevo","{unread} nuevos comentarios","{unread} nuevos comentarios"],
     "Comment" : "Comentario",
+    "An error occurred while trying to edit the comment" : "Ocurrió un error al intentar editar el comentario",
+    "Comment deleted" : "Comentario borrado",
+    "An error occurred while trying to delete the comment" : "Ocurrió un error intentando borrar el comentario",
+    "An error occurred while trying to create the comment" : "Ocurrió un error al intentar crear el comentario",
+    "You were mentioned on \"{file}\", in a comment by a user that has since been deleted" : "Fue mencionado en \"{file}\", en un comentario realizado por un usuario que ha sido eliminado",
     "_%n unread comment_::_%n unread comments_" : ["%n comentarios sin leer","%n comentarios sin leer","%n comentarios sin leer"]
 },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
 }

+ 1 - 0
apps/comments/l10n/fr.js

@@ -9,6 +9,7 @@ OC.L10N.register(
     "%1$s commented on %2$s" : "%1$s a commenté %2$s",
     "{author} commented on {file}" : "{author} a commenté sur {file}",
     "<strong>Comments</strong> for files" : "<strong>Commentaires</strong> sur les fichiers",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Vous avez été mentionné sur « {file} », dans un commentaire par un compte qui depuis a été supprimé",
     "{user} mentioned you in a comment on \"{file}\"" : "{user} vous a mentionné dans un commentaire sur \"{file}\"",
     "Files app plugin to add comments to files" : "Plugin Fichiers app pour ajouter des commentaires aux fichiers",
     "Edit comment" : "Modifier le commentaire",

+ 1 - 0
apps/comments/l10n/fr.json

@@ -7,6 +7,7 @@
     "%1$s commented on %2$s" : "%1$s a commenté %2$s",
     "{author} commented on {file}" : "{author} a commenté sur {file}",
     "<strong>Comments</strong> for files" : "<strong>Commentaires</strong> sur les fichiers",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Vous avez été mentionné sur « {file} », dans un commentaire par un compte qui depuis a été supprimé",
     "{user} mentioned you in a comment on \"{file}\"" : "{user} vous a mentionné dans un commentaire sur \"{file}\"",
     "Files app plugin to add comments to files" : "Plugin Fichiers app pour ajouter des commentaires aux fichiers",
     "Edit comment" : "Modifier le commentaire",

+ 2 - 2
apps/comments/l10n/he.js

@@ -19,7 +19,7 @@ OC.L10N.register(
     "No more messages" : "אין יותר הודעות",
     "Retry" : "ניסיון חוזר",
     "Unable to load the comments list" : "לא ניתן לטעון את רשימת התגובות",
-    "_1 new comment_::_{unread} new comments_" : ["הערה חדשה אחת","{unread} הערות חדשות","{unread} הערות חדשות","{unread} הערות חדשות"],
+    "_1 new comment_::_{unread} new comments_" : ["הערה חדשה אחת","{unread} הערות חדשות","{unread} הערות חדשות"],
     "Comment" : "תגובה",
     "An error occurred while trying to edit the comment" : "אירעה שגיאה בניסיון לערוך את התגובה",
     "Comment deleted" : "נמחקה הערה",
@@ -28,4 +28,4 @@ OC.L10N.register(
     "You were mentioned on \"{file}\", in a comment by a user that has since been deleted" : "אוזכרת בקובץ „{file}”, בהערה על ידי משתמש שנמחק מאז",
     "_%n unread comment_::_%n unread comments_" : ["תגובה אחת שלא נקראה","%n תגובות שלא נקראו","%n תגובות שלא נקראו","%n תגובות שלא נקראו"]
 },
-"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;");
+"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;");

+ 2 - 2
apps/comments/l10n/he.json

@@ -17,7 +17,7 @@
     "No more messages" : "אין יותר הודעות",
     "Retry" : "ניסיון חוזר",
     "Unable to load the comments list" : "לא ניתן לטעון את רשימת התגובות",
-    "_1 new comment_::_{unread} new comments_" : ["הערה חדשה אחת","{unread} הערות חדשות","{unread} הערות חדשות","{unread} הערות חדשות"],
+    "_1 new comment_::_{unread} new comments_" : ["הערה חדשה אחת","{unread} הערות חדשות","{unread} הערות חדשות"],
     "Comment" : "תגובה",
     "An error occurred while trying to edit the comment" : "אירעה שגיאה בניסיון לערוך את התגובה",
     "Comment deleted" : "נמחקה הערה",
@@ -25,5 +25,5 @@
     "An error occurred while trying to create the comment" : "אירעה שגיאה בניסיון ליצור את התגובה",
     "You were mentioned on \"{file}\", in a comment by a user that has since been deleted" : "אוזכרת בקובץ „{file}”, בהערה על ידי משתמש שנמחק מאז",
     "_%n unread comment_::_%n unread comments_" : ["תגובה אחת שלא נקראה","%n תגובות שלא נקראו","%n תגובות שלא נקראו","%n תגובות שלא נקראו"]
-},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;"
+},"pluralForm" :"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;"
 }

+ 1 - 0
apps/comments/l10n/ko.js

@@ -9,6 +9,7 @@ OC.L10N.register(
     "%1$s commented on %2$s" : "%2$s에 %1$s 님이 댓글 남김",
     "{author} commented on {file}" : "{author} 님이 {file}에 댓글 남김",
     "<strong>Comments</strong> for files" : "파일의 <strong>댓글</strong>",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "삭제된 계정이 게시한 “{file}”의 댓글에서 나를 언급함",
     "{user} mentioned you in a comment on \"{file}\"" : "{user} 님이 “{file}”에 남긴 댓글에서 나를 언급함",
     "Files app plugin to add comments to files" : "파일에 댓글을 남기는 파일 앱 플러그인",
     "Edit comment" : "댓글 편집",

+ 1 - 0
apps/comments/l10n/ko.json

@@ -7,6 +7,7 @@
     "%1$s commented on %2$s" : "%2$s에 %1$s 님이 댓글 남김",
     "{author} commented on {file}" : "{author} 님이 {file}에 댓글 남김",
     "<strong>Comments</strong> for files" : "파일의 <strong>댓글</strong>",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "삭제된 계정이 게시한 “{file}”의 댓글에서 나를 언급함",
     "{user} mentioned you in a comment on \"{file}\"" : "{user} 님이 “{file}”에 남긴 댓글에서 나를 언급함",
     "Files app plugin to add comments to files" : "파일에 댓글을 남기는 파일 앱 플러그인",
     "Edit comment" : "댓글 편집",

+ 2 - 0
apps/comments/l10n/pt_BR.js

@@ -9,12 +9,14 @@ OC.L10N.register(
     "%1$s commented on %2$s" : "%1$s comentaram em %2$s",
     "{author} commented on {file}" : "{author} comentou em {file}",
     "<strong>Comments</strong> for files" : "<strong>Comentários</strong> para arquivos",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Você foi mencionado em \"{file}\", em um comentário de uma conta que já foi excluída",
     "{user} mentioned you in a comment on \"{file}\"" : "{user} mencionou você em um comentário em \"{file}\"",
     "Files app plugin to add comments to files" : "Complemento do aplicativo Files para adicionar comentários",
     "Edit comment" : "Editar comentário",
     "Delete comment" : "Excluir comentário",
     "Cancel edit" : "Cancelar edição",
     "New comment" : "Novo comentário",
+    "Write a comment …" : "Escreva um comentário …",
     "Post comment" : "Postar comentário",
     "@ for mentions, : for emoji, / for smart picker" : "@ para menções, : para emoji, / para seletor inteligente",
     "Could not reload comments" : "Não foi possível recarregar comentários",

+ 2 - 0
apps/comments/l10n/pt_BR.json

@@ -7,12 +7,14 @@
     "%1$s commented on %2$s" : "%1$s comentaram em %2$s",
     "{author} commented on {file}" : "{author} comentou em {file}",
     "<strong>Comments</strong> for files" : "<strong>Comentários</strong> para arquivos",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Você foi mencionado em \"{file}\", em um comentário de uma conta que já foi excluída",
     "{user} mentioned you in a comment on \"{file}\"" : "{user} mencionou você em um comentário em \"{file}\"",
     "Files app plugin to add comments to files" : "Complemento do aplicativo Files para adicionar comentários",
     "Edit comment" : "Editar comentário",
     "Delete comment" : "Excluir comentário",
     "Cancel edit" : "Cancelar edição",
     "New comment" : "Novo comentário",
+    "Write a comment …" : "Escreva um comentário …",
     "Post comment" : "Postar comentário",
     "@ for mentions, : for emoji, / for smart picker" : "@ para menções, : para emoji, / para seletor inteligente",
     "Could not reload comments" : "Não foi possível recarregar comentários",

+ 6 - 0
apps/comments/l10n/sk.js

@@ -9,15 +9,21 @@ OC.L10N.register(
     "%1$s commented on %2$s" : "%1$s komentoval %2$s",
     "{author} commented on {file}" : "{author} komentoval {file}",
     "<strong>Comments</strong> for files" : "<strong>Komentáre</strong> pre súbory",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Boli ste spomenutý v \"{file}\", v komentári užívateľom ktorý bol už vymazaný",
     "{user} mentioned you in a comment on \"{file}\"" : "{user} vás spomenul v komentári k “{file}”",
     "Files app plugin to add comments to files" : "Zásuvný modul aplikácie súborov, ktorý umožňuje súborom pridávať komentáre",
     "Edit comment" : "Upraviť komentár",
     "Delete comment" : "Zmazať komentár",
     "Cancel edit" : "Zrušiť upravovanie",
+    "New comment" : "Nový komentár",
+    "Write a comment …" : "Napísať komentár ...",
     "Post comment" : "Odoslať komentár",
+    "@ for mentions, : for emoji, / for smart picker" : "@ pre spomienky, : pre emotikony, / pre inteligentný výber",
+    "Could not reload comments" : "Nepodarilo sa obnoviť komentáre",
     "No comments yet, start the conversation!" : "Žiadne komentáre, začnite konverzáciu!",
     "No more messages" : "Žiadne ďaĺšie správy",
     "Retry" : "Skúsiť znova",
+    "Failed to mark comments as read" : "Nepodarilo sa označiť komentáre ako prečítané.",
     "Unable to load the comments list" : "Nie je možné načítať zoznam komentárov",
     "_1 new comment_::_{unread} new comments_" : ["1 nový komentár","{unread} nové komentáre","{unread} nových komentárov","{unread} nových komentárov"],
     "Comment" : "Komentár",

+ 6 - 0
apps/comments/l10n/sk.json

@@ -7,15 +7,21 @@
     "%1$s commented on %2$s" : "%1$s komentoval %2$s",
     "{author} commented on {file}" : "{author} komentoval {file}",
     "<strong>Comments</strong> for files" : "<strong>Komentáre</strong> pre súbory",
+    "You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Boli ste spomenutý v \"{file}\", v komentári užívateľom ktorý bol už vymazaný",
     "{user} mentioned you in a comment on \"{file}\"" : "{user} vás spomenul v komentári k “{file}”",
     "Files app plugin to add comments to files" : "Zásuvný modul aplikácie súborov, ktorý umožňuje súborom pridávať komentáre",
     "Edit comment" : "Upraviť komentár",
     "Delete comment" : "Zmazať komentár",
     "Cancel edit" : "Zrušiť upravovanie",
+    "New comment" : "Nový komentár",
+    "Write a comment …" : "Napísať komentár ...",
     "Post comment" : "Odoslať komentár",
+    "@ for mentions, : for emoji, / for smart picker" : "@ pre spomienky, : pre emotikony, / pre inteligentný výber",
+    "Could not reload comments" : "Nepodarilo sa obnoviť komentáre",
     "No comments yet, start the conversation!" : "Žiadne komentáre, začnite konverzáciu!",
     "No more messages" : "Žiadne ďaĺšie správy",
     "Retry" : "Skúsiť znova",
+    "Failed to mark comments as read" : "Nepodarilo sa označiť komentáre ako prečítané.",
     "Unable to load the comments list" : "Nie je možné načítať zoznam komentárov",
     "_1 new comment_::_{unread} new comments_" : ["1 nový komentár","{unread} nové komentáre","{unread} nových komentárov","{unread} nových komentárov"],
     "Comment" : "Komentár",

+ 16 - 9
apps/comments/src/services/DavClient.js

@@ -22,16 +22,23 @@
 
 import { createClient } from 'webdav'
 import { getRootPath } from '../utils/davUtils.js'
-import { getRequestToken } from '@nextcloud/auth'
+import { getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
 
 // init webdav client
-const client = createClient(getRootPath(), {
-	headers: {
-		// Add this so the server knows it is an request from the browser
-		'X-Requested-With': 'XMLHttpRequest',
-		// Inject user auth
-		requesttoken: getRequestToken() ?? '',
-	},
-})
+const client = createClient(getRootPath())
+
+// set CSRF token header
+const setHeaders = (token) => {
+  client.setHeaders({
+    // Add this so the server knows it is an request from the browser
+    'X-Requested-With': 'XMLHttpRequest',
+    // Inject user auth
+    requesttoken: token ?? '',
+  })
+}
+
+// refresh headers when request token changes
+onRequestTokenUpdate(setHeaders)
+setHeaders(getRequestToken())
 
 export default client

+ 4 - 6
apps/comments/src/services/GetComments.ts

@@ -23,8 +23,8 @@
 import { parseXML, type DAVResult, type FileStat, type ResponseDataDetailed } from 'webdav'
 
 // https://github.com/perry-mitchell/webdav-client/issues/339
-import { processResponsePayload } from '../../../../node_modules/webdav/dist/node/response.js'
-import { prepareFileFromProps } from '../../../../node_modules/webdav/dist/node/tools/dav.js'
+import { processResponsePayload } from 'webdav/dist/node/response.js'
+import { prepareFileFromProps } from 'webdav/dist/node/tools/dav.js'
 import client from './DavClient.js'
 
 export const DEFAULT_LIMIT = 20
@@ -77,10 +77,8 @@ const getDirectoryFiles = function(
 	// Map all items to a consistent output structure (results)
 	return responseItems.map(item => {
 		// Each item should contain a stat object
-		const {
-			propstat: { prop: props },
-		} = item
+		const props = item.propstat!.prop!;
 
-		return prepareFileFromProps(props, props.id.toString(), isDetailed)
+		return prepareFileFromProps(props, props.id!.toString(), isDetailed)
 	})
 }

+ 2 - 0
apps/contactsinteraction/l10n/ast.js

@@ -3,6 +3,8 @@ OC.L10N.register(
     {
     "Recently contacted" : "Contautos de recién",
     "Contacts Interaction" : "Interaición con contautos",
+    "Manages interaction between accounts and contacts" : "Xestiona la interaición ente cuentes y contautos",
+    "Collect data about accounts and contacts interactions and provide an address book for the data" : "Recueye datos de les interaiciones de cuentes y contautos y forne una llibreta de direiciones colos datos",
     "Manages interaction between users and contacts" : "Xestiona la interaición ente usuarios y contautos",
     "Collect data about user and contacts interactions and provide an address book for the data" : "Recueye datos de les interaiciones d'usuarios y contautos y forne una llibreta de direiciones colos datos"
 },

+ 2 - 0
apps/contactsinteraction/l10n/ast.json

@@ -1,6 +1,8 @@
 { "translations": {
     "Recently contacted" : "Contautos de recién",
     "Contacts Interaction" : "Interaición con contautos",
+    "Manages interaction between accounts and contacts" : "Xestiona la interaición ente cuentes y contautos",
+    "Collect data about accounts and contacts interactions and provide an address book for the data" : "Recueye datos de les interaiciones de cuentes y contautos y forne una llibreta de direiciones colos datos",
     "Manages interaction between users and contacts" : "Xestiona la interaición ente usuarios y contautos",
     "Collect data about user and contacts interactions and provide an address book for the data" : "Recueye datos de les interaiciones d'usuarios y contautos y forne una llibreta de direiciones colos datos"
 },"pluralForm" :"nplurals=2; plural=(n != 1);"

+ 2 - 0
apps/contactsinteraction/l10n/fr.js

@@ -3,6 +3,8 @@ OC.L10N.register(
     {
     "Recently contacted" : "Contacté récemment",
     "Contacts Interaction" : "Interaction des contacts",
+    "Manages interaction between accounts and contacts" : "Gère l'interaction entre les comptes et les contacts",
+    "Collect data about accounts and contacts interactions and provide an address book for the data" : "Recueillir des données sur les interactions des comptes et des contacts et fournir un carnet d'adresses pour les données",
     "Manages interaction between users and contacts" : "Gère l'interaction entre les utilisateurs et les contacts",
     "Collect data about user and contacts interactions and provide an address book for the data" : "Recueillir des données sur les interactions des utilisateurs et des contacts et fournir un carnet d'adresses pour les données"
 },

+ 2 - 0
apps/contactsinteraction/l10n/fr.json

@@ -1,6 +1,8 @@
 { "translations": {
     "Recently contacted" : "Contacté récemment",
     "Contacts Interaction" : "Interaction des contacts",
+    "Manages interaction between accounts and contacts" : "Gère l'interaction entre les comptes et les contacts",
+    "Collect data about accounts and contacts interactions and provide an address book for the data" : "Recueillir des données sur les interactions des comptes et des contacts et fournir un carnet d'adresses pour les données",
     "Manages interaction between users and contacts" : "Gère l'interaction entre les utilisateurs et les contacts",
     "Collect data about user and contacts interactions and provide an address book for the data" : "Recueillir des données sur les interactions des utilisateurs et des contacts et fournir un carnet d'adresses pour les données"
 },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"

+ 1 - 1
apps/contactsinteraction/l10n/he.js

@@ -6,4 +6,4 @@ OC.L10N.register(
     "Manages interaction between users and contacts" : "מנהל אינטראקציה בין משתמשים ואנשי קשר",
     "Collect data about user and contacts interactions and provide an address book for the data" : "אוסף נתונים על אינטראקציות של משתמשים ואנשי קשר, ומספק פנקס כתובות לנתונים"
 },
-"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;");
+"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;");

+ 1 - 1
apps/contactsinteraction/l10n/he.json

@@ -3,5 +3,5 @@
     "Contacts Interaction" : "אינטראקציה בין אנשי קשר",
     "Manages interaction between users and contacts" : "מנהל אינטראקציה בין משתמשים ואנשי קשר",
     "Collect data about user and contacts interactions and provide an address book for the data" : "אוסף נתונים על אינטראקציות של משתמשים ואנשי קשר, ומספק פנקס כתובות לנתונים"
-},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;"
+},"pluralForm" :"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;"
 }

+ 2 - 0
apps/contactsinteraction/l10n/pt_BR.js

@@ -3,6 +3,8 @@ OC.L10N.register(
     {
     "Recently contacted" : "Contactados recentemente",
     "Contacts Interaction" : "Interação de contatos",
+    "Manages interaction between accounts and contacts" : "Gerencia a interação entre contas e contatos",
+    "Collect data about accounts and contacts interactions and provide an address book for the data" : "Colete dados sobre interações de contas e contatos e forneça um catálogo de endereços para os dados",
     "Manages interaction between users and contacts" : "Gerenciar interação entre usuários e contatos",
     "Collect data about user and contacts interactions and provide an address book for the data" : "Coletar dados sobre usuários e interação de contatos e prover um livro de endereços para o dado"
 },

+ 2 - 0
apps/contactsinteraction/l10n/pt_BR.json

@@ -1,6 +1,8 @@
 { "translations": {
     "Recently contacted" : "Contactados recentemente",
     "Contacts Interaction" : "Interação de contatos",
+    "Manages interaction between accounts and contacts" : "Gerencia a interação entre contas e contatos",
+    "Collect data about accounts and contacts interactions and provide an address book for the data" : "Colete dados sobre interações de contas e contatos e forneça um catálogo de endereços para os dados",
     "Manages interaction between users and contacts" : "Gerenciar interação entre usuários e contatos",
     "Collect data about user and contacts interactions and provide an address book for the data" : "Coletar dados sobre usuários e interação de contatos e prover um livro de endereços para o dado"
 },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"

+ 1 - 1
apps/dashboard/l10n/he.js

@@ -21,4 +21,4 @@ OC.L10N.register(
     "Hello" : "שלום",
     "Hello, {name}" : "שלום, {name}"
 },
-"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;");
+"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;");

+ 1 - 1
apps/dashboard/l10n/he.json

@@ -18,5 +18,5 @@
     "Good evening, {name}" : "ערב טוב, {name}",
     "Hello" : "שלום",
     "Hello, {name}" : "שלום, {name}"
-},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;"
+},"pluralForm" :"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;"
 }

+ 1 - 0
apps/dashboard/l10n/pt_BR.js

@@ -3,6 +3,7 @@ OC.L10N.register(
     {
     "Dashboard" : "Painel",
     "Dashboard app" : "Aplicativo Painel",
+    "Start your day informed\n\nThe Nextcloud Dashboard is your starting point of the day, giving you an overview of your upcoming appointments, urgent emails, chat messages, incoming tickets, latest tweets and much more! People can add the widgets they like and change the background to their liking." : "Comece o dia informado \n\nO Nextcloud Dashboard é o seu ponto de partida do dia, oferecendo uma visão geral de seus próximos compromissos, e-mails urgentes, mensagens de bate-papo, tickets recebidos, tweets mais recentes e muito mais! As pessoas podem adicionar os widgets que desejarem e alterar o plano de fundo de acordo com sua preferência.",
     "\"{title} icon\"" : "\"{title} icon\"",
     "Customize" : "Personalizar",
     "Edit widgets" : "Editar widgets",

+ 1 - 0
apps/dashboard/l10n/pt_BR.json

@@ -1,6 +1,7 @@
 { "translations": {
     "Dashboard" : "Painel",
     "Dashboard app" : "Aplicativo Painel",
+    "Start your day informed\n\nThe Nextcloud Dashboard is your starting point of the day, giving you an overview of your upcoming appointments, urgent emails, chat messages, incoming tickets, latest tweets and much more! People can add the widgets they like and change the background to their liking." : "Comece o dia informado \n\nO Nextcloud Dashboard é o seu ponto de partida do dia, oferecendo uma visão geral de seus próximos compromissos, e-mails urgentes, mensagens de bate-papo, tickets recebidos, tweets mais recentes e muito mais! As pessoas podem adicionar os widgets que desejarem e alterar o plano de fundo de acordo com sua preferência.",
     "\"{title} icon\"" : "\"{title} icon\"",
     "Customize" : "Personalizar",
     "Edit widgets" : "Editar widgets",

+ 3 - 14
apps/dashboard/lib/Controller/DashboardApiController.php

@@ -54,25 +54,14 @@ use OCP\IRequest;
  */
 class DashboardApiController extends OCSController {
 
-	/** @var IManager */
-	private $dashboardManager;
-	/** @var IConfig */
-	private $config;
-	/** @var string|null */
-	private $userId;
-
 	public function __construct(
 		string $appName,
 		IRequest $request,
-		IManager $dashboardManager,
-		IConfig $config,
-		?string $userId
+		private IManager $dashboardManager,
+		private IConfig $config,
+		private ?string $userId,
 	) {
 		parent::__construct($appName, $request);
-
-		$this->dashboardManager = $dashboardManager;
-		$this->config = $config;
-		$this->userId = $userId;
 	}
 
 	/**

+ 6 - 26
apps/dashboard/lib/Controller/DashboardController.php

@@ -46,37 +46,17 @@ use OCP\IRequest;
 #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
 class DashboardController extends Controller {
 
-	/** @var IInitialState */
-	private $initialState;
-	/** @var IEventDispatcher */
-	private $eventDispatcher;
-	/** @var IManager */
-	private $dashboardManager;
-	/** @var IConfig */
-	private $config;
-	/** @var IL10N */
-	private $l10n;
-	/** @var string */
-	private $userId;
-
 	public function __construct(
 		string $appName,
 		IRequest $request,
-		IInitialState $initialState,
-		IEventDispatcher $eventDispatcher,
-		IManager $dashboardManager,
-		IConfig $config,
-		IL10N $l10n,
-		$userId
+		private IInitialState $initialState,
+		private IEventDispatcher $eventDispatcher,
+		private IManager $dashboardManager,
+		private IConfig $config,
+		private IL10N $l10n,
+		private ?string $userId
 	) {
 		parent::__construct($appName, $request);
-
-		$this->initialState = $initialState;
-		$this->eventDispatcher = $eventDispatcher;
-		$this->dashboardManager = $dashboardManager;
-		$this->config = $config;
-		$this->l10n = $l10n;
-		$this->userId = $userId;
 	}
 
 	/**

+ 2 - 8
apps/dashboard/lib/Controller/LayoutApiController.php

@@ -31,21 +31,15 @@ use OCP\IConfig;
 use OCP\IRequest;
 
 class LayoutApiController extends OCSController {
-	/** @var IConfig */
-	private $config;
-	/** @var string */
-	private $userId;
 
 	public function __construct(
 		string $appName,
 		IRequest $request,
-		IConfig $config,
-		$userId
+		private IConfig $config,
+		private ?string $userId,
 	) {
 		parent::__construct($appName, $request);
 
-		$this->config = $config;
-		$this->userId = $userId;
 	}
 
 	/**

+ 1 - 0
apps/dav/appinfo/info.xml

@@ -50,6 +50,7 @@
 		<command>OCA\DAV\Command\CreateAddressBook</command>
 		<command>OCA\DAV\Command\CreateCalendar</command>
 		<command>OCA\DAV\Command\DeleteCalendar</command>
+		<command>OCA\DAV\Command\FixCalendarSyncCommand</command>
 		<command>OCA\DAV\Command\MoveCalendar</command>
 		<command>OCA\DAV\Command\ListCalendars</command>
 		<command>OCA\DAV\Command\RetentionCleanupCommand</command>

+ 1 - 0
apps/dav/composer/composer/autoload_classmap.php

@@ -141,6 +141,7 @@ return array(
     'OCA\\DAV\\Command\\CreateAddressBook' => $baseDir . '/../lib/Command/CreateAddressBook.php',
     'OCA\\DAV\\Command\\CreateCalendar' => $baseDir . '/../lib/Command/CreateCalendar.php',
     'OCA\\DAV\\Command\\DeleteCalendar' => $baseDir . '/../lib/Command/DeleteCalendar.php',
+    'OCA\\DAV\\Command\\FixCalendarSyncCommand' => $baseDir . '/../lib/Command/FixCalendarSyncCommand.php',
     'OCA\\DAV\\Command\\ListCalendars' => $baseDir . '/../lib/Command/ListCalendars.php',
     'OCA\\DAV\\Command\\MoveCalendar' => $baseDir . '/../lib/Command/MoveCalendar.php',
     'OCA\\DAV\\Command\\RemoveInvalidShares' => $baseDir . '/../lib/Command/RemoveInvalidShares.php',

+ 1 - 0
apps/dav/composer/composer/autoload_static.php

@@ -156,6 +156,7 @@ class ComposerStaticInitDAV
         'OCA\\DAV\\Command\\CreateAddressBook' => __DIR__ . '/..' . '/../lib/Command/CreateAddressBook.php',
         'OCA\\DAV\\Command\\CreateCalendar' => __DIR__ . '/..' . '/../lib/Command/CreateCalendar.php',
         'OCA\\DAV\\Command\\DeleteCalendar' => __DIR__ . '/..' . '/../lib/Command/DeleteCalendar.php',
+        'OCA\\DAV\\Command\\FixCalendarSyncCommand' => __DIR__ . '/..' . '/../lib/Command/FixCalendarSyncCommand.php',
         'OCA\\DAV\\Command\\ListCalendars' => __DIR__ . '/..' . '/../lib/Command/ListCalendars.php',
         'OCA\\DAV\\Command\\MoveCalendar' => __DIR__ . '/..' . '/../lib/Command/MoveCalendar.php',
         'OCA\\DAV\\Command\\RemoveInvalidShares' => __DIR__ . '/..' . '/../lib/Command/RemoveInvalidShares.php',

+ 114 - 0
apps/dav/l10n/ast.js

@@ -0,0 +1,114 @@
+OC.L10N.register(
+    "dav",
+    {
+    "Calendar" : "Calendariu",
+    "Tasks" : "Xeres",
+    "Personal" : "Personal",
+    "{actor} created calendar {calendar}" : "{actor} creó'l calendariu «{calendar}»",
+    "You created calendar {calendar}" : "Creesti'l calendariu «{calendar}»",
+    "{actor} deleted calendar {calendar}" : "{actor} desanició'l calendariu «{calendar}»",
+    "You deleted calendar {calendar}" : "Desaniciesti'l calendariu «{calendar}»",
+    "{actor} updated calendar {calendar}" : "{actor} anovó'l calendariu «{calendar}»",
+    "You updated calendar {calendar}" : "Anovesti'l calendariu «{calendar}»",
+    "{actor} restored calendar {calendar}" : "{actor} restauró'l calendariu «{calendar}»",
+    "You restored calendar {calendar}" : "Restauresti'l calendariu «{calendar}»",
+    "You removed public link for calendar {calendar}" : "Desaniciesti l'enllaz públicu del calendariu «{calendar}»",
+    "{actor} shared calendar {calendar} with you" : "{actor} compartió'l calendariu «{calendar}» contigo",
+    "You shared calendar {calendar} with {user}" : "Compartiesti'l calendariu «{calendar}» con {user}",
+    "{actor} shared calendar {calendar} with {user}" : "{actor} compartió'l calendariu «{calendar}» con {user}",
+    "{actor} unshared calendar {calendar} from you" : "{actor} dexó de compartir el calendariu «{calendar}» contigo",
+    "You unshared calendar {calendar} from {user}" : "Dexesti de compartir el calendariu «{calendar}» con «{user}»",
+    "{actor} unshared calendar {calendar} from {user}" : "{actor} dexó de compartir el calendariu «{calendar}» con «{user}»",
+    "{actor} unshared calendar {calendar} from themselves" : "{actor} dexó de compartir el calendariu «{calendar}» con sigo",
+    "You shared calendar {calendar} with group {group}" : "Compartiesti'l calendariu «{calendar}» col grupu «{gorup}»",
+    "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió'l calendariu «{calendar}» col grupu «{group}»",
+    "You unshared calendar {calendar} from group {group}" : "Dexesti de compartir el calendariu «{calendar}» col grupu «{group}»",
+    "{actor} unshared calendar {calendar} from group {group}" : "{actor} dexó de compartir el calendariu «{calendar}» col grupu «{group}»",
+    "Untitled event" : "Eventu ensin títulu",
+    "{actor} created event {event} in calendar {calendar}" : "{actor} creó l'eventu «{event}» nel calendariu «{calendar}»",
+    "You created event {event} in calendar {calendar}" : "Creesti l'eventu «{event}» nel calendariu «{calendar}»",
+    "{actor} deleted event {event} from calendar {calendar}" : "{actor} desanició l'eventu «{event}» del calendariu «{calendar}»",
+    "You deleted event {event} from calendar {calendar}" : "Desaniciesti l'eventu «{event}» del calendariu «{calendar}»",
+    "{actor} updated event {event} in calendar {calendar}" : "{actor} anovó l'eventu {event} nel calendariu {calendar}",
+    "You updated event {event} in calendar {calendar}" : "Anovesti l'eventu {event} nel calendariu {calendar}",
+    "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} movió l'eventu «{event}» del calendariu «{sourceCalendar}» al calendariu «{targetCalendar}»",
+    "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moviesti l'eventu «{event}» del calendariu «{sourceCalendar}» al calendariu «{targetCalendar}»",
+    "{actor} restored event {event} of calendar {calendar}" : "{actor} restauró l'eventu «{event}» del calendariu «{calendar}»",
+    "You restored event {event} of calendar {calendar}" : "Restauresti l'eventu «{event}» del calendariu «{calendar}»",
+    "{actor} created to-do {todo} in list {calendar}" : "{actor} creó la xera pendiente «{todo}» na llista «{calendar}»",
+    "You created to-do {todo} in list {calendar}" : "Creesti la xera pendiente «{todo}» na llista «{calendar}»",
+    "{actor} deleted to-do {todo} from list {calendar}" : "{actor} desanició la xera pendiente «{todo}» de la llista «{calendar}»",
+    "You deleted to-do {todo} from list {calendar}" : "Desaniciesti la xera pendiente «{todo}» de la llista «{calendar}»",
+    "{actor} updated to-do {todo} in list {calendar}" : "{actor} anovó la xera pendiente «{todo}» de la llista «{calendar}»",
+    "You updated to-do {todo} in list {calendar}" : "Anovesti la xera pendiente «{todo}» de la llista «{calendar}»",
+    "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolvió la xera pendiente «{todo}» de la llista «{calendar}»",
+    "You solved to-do {todo} in list {calendar}" : "Resolviesti la xera pendiente «{todo}» de la llista «{calendar}»",
+    "{actor} reopened to-do {todo} in list {calendar}" : "{actor} volvió abrir la xera pendiente «{todo}» de la llista «{calendar}»",
+    "You reopened to-do {todo} in list {calendar}" : "Volviesti abrir la xera pendiente «{todo}» de la llista «{calendar}»",
+    "A <strong>calendar</strong> was modified" : "Modificóse un <strong>calendariu</strong>",
+    "A calendar <strong>event</strong> was modified" : "Modificóse un elementu del <strong>calendariu</strong>",
+    "A calendar <strong>to-do</strong> was modified" : "Modificóse una <strong>xera pendiente</strong> del calendariu",
+    "Untitled calendar" : "Calendariu ensin títulu",
+    "Calendar:" : "Calendariu:",
+    "Date:" : "Data:",
+    "Description:" : "Descripción:",
+    "_%n year_::_%n years_" : ["%n añu","%n años"],
+    "_%n month_::_%n months_" : ["%n mes","%n meses"],
+    "_%n day_::_%n days_" : ["%n día","%n díes"],
+    "_%n hour_::_%n hours_" : ["%n hora","%n hores"],
+    "_%n minute_::_%n minutes_" : ["%n minutu","%n minutos"],
+    "Calendar: %s" : "Calendariu: %s",
+    "Date: %s" : "Data: %s",
+    "Description: %s" : "Descripción: %s",
+    "%1$s via %2$s" : "%1$s per %2$s",
+    "%1$s has accepted your invitation" : "%1$s aceptó la to invitación",
+    "%1$s has declined your invitation" : "%1$s refugó la to invitación",
+    "%1$s has responded to your invitation" : "%1$s respondió a la to invitación",
+    "Invitation updated: %1$s" : "Anovóse la invitación: %1$s",
+    "%1$s updated the event \"%2$s\"" : "%1$s anovó l'eventu «%2$s»",
+    "Invitation: %1$s" : "Invitación: %1$s",
+    "%1$s would like to invite you to \"%2$s\"" : "%1$s quier convidate a «%2$s»",
+    "Organizer:" : "Organizador:",
+    "Attendees:" : "Asistentes:",
+    "Title:" : "Títulu:",
+    "Time:" : "Hora:",
+    "Location:" : "Llugar:",
+    "Link:" : "Enllaz:",
+    "Accept" : "Aceptar",
+    "Decline" : "Refugar",
+    "More options …" : "Más opciones…",
+    "Contacts" : "Contautos",
+    "Accounts" : "Cuentes",
+    "File is not updatable: %1$s" : "El ficheru nun se pue anovar: %1$s",
+    "Could not write file contents" : "Nun se pudo escribir los conteníos del ficheru",
+    "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+    "Could not open file" : "Nun se pudo abrir el ficheru",
+    "Encryption not ready: %1$s" : "El cifráu nun ta preparáu: %1$s",
+    "Failed to open file: %1$s" : "Nun se pue abrir el ficheru: %1$s",
+    "Failed to unlink: %1$s" : "Nun se pue desenllaciar: %1$s",
+    "Failed to write file contents: %1$s" : "Nun se pue escribir el conteníu nel ficheru: %1$s",
+    "File not found: %1$s" : "Nun s'atopó'l ficheru: %1$s",
+    "Configures a CalDAV account" : "Configura una cuenta CalDAV",
+    "Configures a CardDAV account" : "Configura una cuenta CardDAV",
+    "Events" : "Eventos",
+    "Untitled task" : "Xera ensin títulu",
+    "Contacts and groups" : "Contautos y grupos",
+    "WebDAV" : "WebDAV",
+    "WebDAV endpoint" : "Estremu de WebDAV",
+    "Save" : "Guardar",
+    "Time zone:" : "Fusu horariu:",
+    "Monday" : "Llunes",
+    "Tuesday" : "Martes",
+    "Wednesday" : "Miércoles",
+    "Thursday" : "Xueves",
+    "Friday" : "Vienres",
+    "Saturday" : "Sábadu",
+    "Sunday" : "Domingu",
+    "Failed to load availability" : "Nun se pue cargar la disponibilidá",
+    "Failed to save availability" : "Nun se pue guardar la disponibilidá",
+    "Availability" : "Disponibilidá",
+    "Calendar server" : "Sirvidor de calendarios",
+    "Are you accepting the invitation?" : "¿Aceptes la invitación?",
+    "Your attendance was updated successfully." : "La to asistencia anovóse correutamente."
+},
+"nplurals=2; plural=(n != 1);");

+ 112 - 0
apps/dav/l10n/ast.json

@@ -0,0 +1,112 @@
+{ "translations": {
+    "Calendar" : "Calendariu",
+    "Tasks" : "Xeres",
+    "Personal" : "Personal",
+    "{actor} created calendar {calendar}" : "{actor} creó'l calendariu «{calendar}»",
+    "You created calendar {calendar}" : "Creesti'l calendariu «{calendar}»",
+    "{actor} deleted calendar {calendar}" : "{actor} desanició'l calendariu «{calendar}»",
+    "You deleted calendar {calendar}" : "Desaniciesti'l calendariu «{calendar}»",
+    "{actor} updated calendar {calendar}" : "{actor} anovó'l calendariu «{calendar}»",
+    "You updated calendar {calendar}" : "Anovesti'l calendariu «{calendar}»",
+    "{actor} restored calendar {calendar}" : "{actor} restauró'l calendariu «{calendar}»",
+    "You restored calendar {calendar}" : "Restauresti'l calendariu «{calendar}»",
+    "You removed public link for calendar {calendar}" : "Desaniciesti l'enllaz públicu del calendariu «{calendar}»",
+    "{actor} shared calendar {calendar} with you" : "{actor} compartió'l calendariu «{calendar}» contigo",
+    "You shared calendar {calendar} with {user}" : "Compartiesti'l calendariu «{calendar}» con {user}",
+    "{actor} shared calendar {calendar} with {user}" : "{actor} compartió'l calendariu «{calendar}» con {user}",
+    "{actor} unshared calendar {calendar} from you" : "{actor} dexó de compartir el calendariu «{calendar}» contigo",
+    "You unshared calendar {calendar} from {user}" : "Dexesti de compartir el calendariu «{calendar}» con «{user}»",
+    "{actor} unshared calendar {calendar} from {user}" : "{actor} dexó de compartir el calendariu «{calendar}» con «{user}»",
+    "{actor} unshared calendar {calendar} from themselves" : "{actor} dexó de compartir el calendariu «{calendar}» con sigo",
+    "You shared calendar {calendar} with group {group}" : "Compartiesti'l calendariu «{calendar}» col grupu «{gorup}»",
+    "{actor} shared calendar {calendar} with group {group}" : "{actor} compartió'l calendariu «{calendar}» col grupu «{group}»",
+    "You unshared calendar {calendar} from group {group}" : "Dexesti de compartir el calendariu «{calendar}» col grupu «{group}»",
+    "{actor} unshared calendar {calendar} from group {group}" : "{actor} dexó de compartir el calendariu «{calendar}» col grupu «{group}»",
+    "Untitled event" : "Eventu ensin títulu",
+    "{actor} created event {event} in calendar {calendar}" : "{actor} creó l'eventu «{event}» nel calendariu «{calendar}»",
+    "You created event {event} in calendar {calendar}" : "Creesti l'eventu «{event}» nel calendariu «{calendar}»",
+    "{actor} deleted event {event} from calendar {calendar}" : "{actor} desanició l'eventu «{event}» del calendariu «{calendar}»",
+    "You deleted event {event} from calendar {calendar}" : "Desaniciesti l'eventu «{event}» del calendariu «{calendar}»",
+    "{actor} updated event {event} in calendar {calendar}" : "{actor} anovó l'eventu {event} nel calendariu {calendar}",
+    "You updated event {event} in calendar {calendar}" : "Anovesti l'eventu {event} nel calendariu {calendar}",
+    "{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} movió l'eventu «{event}» del calendariu «{sourceCalendar}» al calendariu «{targetCalendar}»",
+    "You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Moviesti l'eventu «{event}» del calendariu «{sourceCalendar}» al calendariu «{targetCalendar}»",
+    "{actor} restored event {event} of calendar {calendar}" : "{actor} restauró l'eventu «{event}» del calendariu «{calendar}»",
+    "You restored event {event} of calendar {calendar}" : "Restauresti l'eventu «{event}» del calendariu «{calendar}»",
+    "{actor} created to-do {todo} in list {calendar}" : "{actor} creó la xera pendiente «{todo}» na llista «{calendar}»",
+    "You created to-do {todo} in list {calendar}" : "Creesti la xera pendiente «{todo}» na llista «{calendar}»",
+    "{actor} deleted to-do {todo} from list {calendar}" : "{actor} desanició la xera pendiente «{todo}» de la llista «{calendar}»",
+    "You deleted to-do {todo} from list {calendar}" : "Desaniciesti la xera pendiente «{todo}» de la llista «{calendar}»",
+    "{actor} updated to-do {todo} in list {calendar}" : "{actor} anovó la xera pendiente «{todo}» de la llista «{calendar}»",
+    "You updated to-do {todo} in list {calendar}" : "Anovesti la xera pendiente «{todo}» de la llista «{calendar}»",
+    "{actor} solved to-do {todo} in list {calendar}" : "{actor} resolvió la xera pendiente «{todo}» de la llista «{calendar}»",
+    "You solved to-do {todo} in list {calendar}" : "Resolviesti la xera pendiente «{todo}» de la llista «{calendar}»",
+    "{actor} reopened to-do {todo} in list {calendar}" : "{actor} volvió abrir la xera pendiente «{todo}» de la llista «{calendar}»",
+    "You reopened to-do {todo} in list {calendar}" : "Volviesti abrir la xera pendiente «{todo}» de la llista «{calendar}»",
+    "A <strong>calendar</strong> was modified" : "Modificóse un <strong>calendariu</strong>",
+    "A calendar <strong>event</strong> was modified" : "Modificóse un elementu del <strong>calendariu</strong>",
+    "A calendar <strong>to-do</strong> was modified" : "Modificóse una <strong>xera pendiente</strong> del calendariu",
+    "Untitled calendar" : "Calendariu ensin títulu",
+    "Calendar:" : "Calendariu:",
+    "Date:" : "Data:",
+    "Description:" : "Descripción:",
+    "_%n year_::_%n years_" : ["%n añu","%n años"],
+    "_%n month_::_%n months_" : ["%n mes","%n meses"],
+    "_%n day_::_%n days_" : ["%n día","%n díes"],
+    "_%n hour_::_%n hours_" : ["%n hora","%n hores"],
+    "_%n minute_::_%n minutes_" : ["%n minutu","%n minutos"],
+    "Calendar: %s" : "Calendariu: %s",
+    "Date: %s" : "Data: %s",
+    "Description: %s" : "Descripción: %s",
+    "%1$s via %2$s" : "%1$s per %2$s",
+    "%1$s has accepted your invitation" : "%1$s aceptó la to invitación",
+    "%1$s has declined your invitation" : "%1$s refugó la to invitación",
+    "%1$s has responded to your invitation" : "%1$s respondió a la to invitación",
+    "Invitation updated: %1$s" : "Anovóse la invitación: %1$s",
+    "%1$s updated the event \"%2$s\"" : "%1$s anovó l'eventu «%2$s»",
+    "Invitation: %1$s" : "Invitación: %1$s",
+    "%1$s would like to invite you to \"%2$s\"" : "%1$s quier convidate a «%2$s»",
+    "Organizer:" : "Organizador:",
+    "Attendees:" : "Asistentes:",
+    "Title:" : "Títulu:",
+    "Time:" : "Hora:",
+    "Location:" : "Llugar:",
+    "Link:" : "Enllaz:",
+    "Accept" : "Aceptar",
+    "Decline" : "Refugar",
+    "More options …" : "Más opciones…",
+    "Contacts" : "Contautos",
+    "Accounts" : "Cuentes",
+    "File is not updatable: %1$s" : "El ficheru nun se pue anovar: %1$s",
+    "Could not write file contents" : "Nun se pudo escribir los conteníos del ficheru",
+    "_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
+    "Could not open file" : "Nun se pudo abrir el ficheru",
+    "Encryption not ready: %1$s" : "El cifráu nun ta preparáu: %1$s",
+    "Failed to open file: %1$s" : "Nun se pue abrir el ficheru: %1$s",
+    "Failed to unlink: %1$s" : "Nun se pue desenllaciar: %1$s",
+    "Failed to write file contents: %1$s" : "Nun se pue escribir el conteníu nel ficheru: %1$s",
+    "File not found: %1$s" : "Nun s'atopó'l ficheru: %1$s",
+    "Configures a CalDAV account" : "Configura una cuenta CalDAV",
+    "Configures a CardDAV account" : "Configura una cuenta CardDAV",
+    "Events" : "Eventos",
+    "Untitled task" : "Xera ensin títulu",
+    "Contacts and groups" : "Contautos y grupos",
+    "WebDAV" : "WebDAV",
+    "WebDAV endpoint" : "Estremu de WebDAV",
+    "Save" : "Guardar",
+    "Time zone:" : "Fusu horariu:",
+    "Monday" : "Llunes",
+    "Tuesday" : "Martes",
+    "Wednesday" : "Miércoles",
+    "Thursday" : "Xueves",
+    "Friday" : "Vienres",
+    "Saturday" : "Sábadu",
+    "Sunday" : "Domingu",
+    "Failed to load availability" : "Nun se pue cargar la disponibilidá",
+    "Failed to save availability" : "Nun se pue guardar la disponibilidá",
+    "Availability" : "Disponibilidá",
+    "Calendar server" : "Sirvidor de calendarios",
+    "Are you accepting the invitation?" : "¿Aceptes la invitación?",
+    "Your attendance was updated successfully." : "La to asistencia anovóse correutamente."
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
+}

+ 2 - 0
apps/dav/l10n/de.js

@@ -170,6 +170,7 @@ OC.L10N.register(
     "Delete slot" : "Slot löschen",
     "No working hours set" : "Keine Arbeitszeiten konfiguriert",
     "Add slot" : "Slot hinzufügen",
+    "Weekdays" : "Wochentage",
     "Monday" : "Montag",
     "Tuesday" : "Dienstag",
     "Wednesday" : "Mittwoch",
@@ -184,6 +185,7 @@ OC.L10N.register(
     "Saved availability" : "Verfügbarkeit gespeichert",
     "Failed to save availability" : "Fehler beim Speichern der Verfügbarkeit",
     "Availability" : "Verfügbarkeit",
+    "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Wenn du deine Arbeitszeiten konfigurierst, können andere Benutzer sehen, wann du nicht im Büro bist, wenn sie eine Besprechung buchen.",
     "Absence" : "Abwesenheit",
     "Configure your next absence period." : "Richte deinen nächsten Abwesenheitszeitraum ein.",
     "Calendar server" : "Kalender-Server",

+ 2 - 0
apps/dav/l10n/de.json

@@ -168,6 +168,7 @@
     "Delete slot" : "Slot löschen",
     "No working hours set" : "Keine Arbeitszeiten konfiguriert",
     "Add slot" : "Slot hinzufügen",
+    "Weekdays" : "Wochentage",
     "Monday" : "Montag",
     "Tuesday" : "Dienstag",
     "Wednesday" : "Mittwoch",
@@ -182,6 +183,7 @@
     "Saved availability" : "Verfügbarkeit gespeichert",
     "Failed to save availability" : "Fehler beim Speichern der Verfügbarkeit",
     "Availability" : "Verfügbarkeit",
+    "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Wenn du deine Arbeitszeiten konfigurierst, können andere Benutzer sehen, wann du nicht im Büro bist, wenn sie eine Besprechung buchen.",
     "Absence" : "Abwesenheit",
     "Configure your next absence period." : "Richte deinen nächsten Abwesenheitszeitraum ein.",
     "Calendar server" : "Kalender-Server",

+ 2 - 0
apps/dav/l10n/pt_BR.js

@@ -170,6 +170,7 @@ OC.L10N.register(
     "Delete slot" : "Excluir slot",
     "No working hours set" : "Sem horário de trabalho definido",
     "Add slot" : "Adicionar slot ",
+    "Weekdays" : "Dias da semana",
     "Monday" : "Segunda-feira",
     "Tuesday" : "Terça-feira",
     "Wednesday" : "Quarta-feira",
@@ -184,6 +185,7 @@ OC.L10N.register(
     "Saved availability" : "Disponibilidade salva",
     "Failed to save availability" : "Falha ao salvar a disponibilidade",
     "Availability" : "Disponibilidade",
+    "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se você configurar seu horário de trabalho, outras pessoas verão quando você estiver ausente quando agendarem uma reunião.",
     "Absence" : "Ausência",
     "Configure your next absence period." : "Configure seu próximo período de ausência",
     "Calendar server" : "Servidor de calendário",

+ 2 - 0
apps/dav/l10n/pt_BR.json

@@ -168,6 +168,7 @@
     "Delete slot" : "Excluir slot",
     "No working hours set" : "Sem horário de trabalho definido",
     "Add slot" : "Adicionar slot ",
+    "Weekdays" : "Dias da semana",
     "Monday" : "Segunda-feira",
     "Tuesday" : "Terça-feira",
     "Wednesday" : "Quarta-feira",
@@ -182,6 +183,7 @@
     "Saved availability" : "Disponibilidade salva",
     "Failed to save availability" : "Falha ao salvar a disponibilidade",
     "Availability" : "Disponibilidade",
+    "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Se você configurar seu horário de trabalho, outras pessoas verão quando você estiver ausente quando agendarem uma reunião.",
     "Absence" : "Ausência",
     "Configure your next absence period." : "Configure seu próximo período de ausência",
     "Calendar server" : "Servidor de calendário",

+ 26 - 0
apps/dav/l10n/sk.js

@@ -75,7 +75,14 @@ OC.L10N.register(
     "Cancelled: %1$s" : "Zrušené: %1$s",
     "\"%1$s\" has been canceled" : "\"%1$s\" bolo zrušené",
     "Re: %1$s" : "Re: %1$s",
+    "%1$s has accepted your invitation" : "%1$s prijal/a vaše pozvanie",
+    "%1$s has tentatively accepted your invitation" : "%1$s predbežne prijal vaše pozvanie.",
+    "%1$s has declined your invitation" : "%1$s odmietol/a vaše pozvanie",
+    "%1$s has responded to your invitation" : "%1$s zareagoval/a na vašu pozvánku",
+    "Invitation updated: %1$s" : "Pozvánka aktualizovaná: %1$s",
+    "%1$s updated the event \"%2$s\"" : "%1$s aktualizoval udalosť \"%2$s\"",
     "Invitation: %1$s" : "Pozvánka: %1$s",
+    "%1$s would like to invite you to \"%2$s\"" : "%1$s by vás chcel/a pozvať na \"%2$s\"",
     "Organizer:" : "Organizátor:",
     "Attendees:" : "Účastníci:",
     "Title:" : "Názov:",
@@ -111,6 +118,8 @@ OC.L10N.register(
     "{actor} updated contact {card} in address book {addressbook}" : "{actor} upravil kontakt {card} v adresári {addressbook}",
     "You updated contact {card} in address book {addressbook}" : "Upravili ste kontakt {card} v adresári {addressbook}",
     "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>kontakt</strong> alebo <strong>adresár</strong> bol upravený",
+    "Accounts" : "Účty",
+    "System address book which holds all accounts" : "Systémový adresár, ktorý obsahuje všetky účty.",
     "File is not updatable: %1$s" : "Súbor nie je možné aktualizovať: %1$s",
     "Could not write to final file, canceled by hook" : "Nepodarilo sa zapísať do konečného súboru, zrušené háčikom (hook)",
     "Could not write file contents" : "Nepodarilo sa zapísať obsah súboru",
@@ -138,18 +147,30 @@ OC.L10N.register(
     "Completed on %s" : "Dokončené %s",
     "Due on %s by %s" : "Termín od %s do %s",
     "Due on %s" : "Termín do %s",
+    "DAV system address book" : "Systémový DAV adresár",
+    "No outstanding DAV system address book sync." : "Žiadna zostávajúca synchronizácia adresára systému DAV.",
+    "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV synchronizácia systémového adresára ešte nebola spustená, pretože vaša inštancia má viac ako 1000 užívateľov alebo sa vyskytla chyba. Prosím, spustite ju manuálne volaním \"occ dav:sync-system-addressbook\".",
     "Migrated calendar (%1$s)" : "Migrovaný kalendár (%1$s)",
     "Calendars including events, details and attendees" : "Kalendáre vrátane udalostí, podrobností a účastníkov",
     "Contacts and groups" : "Kontakty a skupiny",
     "WebDAV" : "WebDAV",
     "WebDAV endpoint" : "Koncový bod WebDAV",
     "First day" : "Prvý deň",
+    "Last day (inclusive)" : "Posledný deň (vrátane)",
+    "Short absence status" : "Status pre Krátku neprítomnosť",
+    "Long absence Message" : "Sprava pri Dlhej neprítomnosti",
     "Save" : "Uložiť",
+    "Disable absence" : "Zakázať neprítomnosť",
+    "Absence saved" : "Neprítomnosť uložená",
+    "Failed to save your absence settings" : "Nepodarilo sa uložiť vaše nastavenia neprítomnosti.",
+    "Absence cleared" : "Neprítomnosť odstránená",
+    "Failed to clear your absence settings" : "Nepodarilo sa vymazať vaše nastavenia neprítomnosti.",
     "Time zone:" : "Časová zóna:",
     "to" : "do",
     "Delete slot" : "Odstrániť slot",
     "No working hours set" : "Nenastavená pracovná doba",
     "Add slot" : "Pridať slot",
+    "Weekdays" : "Pracovné dni",
     "Monday" : "Pondelok",
     "Tuesday" : "Utorok",
     "Wednesday" : "Streda",
@@ -157,11 +178,16 @@ OC.L10N.register(
     "Friday" : "Piatok",
     "Saturday" : "Sobota",
     "Sunday" : "Nedeľa",
+    "Pick a start time for {dayName}" : "Vyberte začiatočný čas pre {dayName}",
+    "Pick a end time for {dayName}" : "Vyberte koncový čas pre {dayName}",
     "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automaticky nastaviť stav používateľa na „Nerušiť“ ak nie ste dostupný,  pre stlmenie všetkých upozornení.",
     "Failed to load availability" : "Nepodarilo sa načítať dostupnosť",
     "Saved availability" : "Dostupnosť bola uložená",
     "Failed to save availability" : "Nepodarilo sa uložiť dostupnosť",
     "Availability" : "Dostupnosť",
+    "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ak nakonfigurujete svoj pracovný čas, ostatní užívatelia vás uvidia ako neprítomného, keď si rezervujete schôdzku",
+    "Absence" : "Neprítomnosť",
+    "Configure your next absence period." : "Nastavte si ďalšie obdobie svojej neprítomnosti.",
     "Calendar server" : "Kalendárový server",
     "Send invitations to attendees" : "Odoslanie pozvánok účastníkom",
     "Automatically generate a birthday calendar" : "Automaticky generovať narodeninový kalendár",

+ 26 - 0
apps/dav/l10n/sk.json

@@ -73,7 +73,14 @@
     "Cancelled: %1$s" : "Zrušené: %1$s",
     "\"%1$s\" has been canceled" : "\"%1$s\" bolo zrušené",
     "Re: %1$s" : "Re: %1$s",
+    "%1$s has accepted your invitation" : "%1$s prijal/a vaše pozvanie",
+    "%1$s has tentatively accepted your invitation" : "%1$s predbežne prijal vaše pozvanie.",
+    "%1$s has declined your invitation" : "%1$s odmietol/a vaše pozvanie",
+    "%1$s has responded to your invitation" : "%1$s zareagoval/a na vašu pozvánku",
+    "Invitation updated: %1$s" : "Pozvánka aktualizovaná: %1$s",
+    "%1$s updated the event \"%2$s\"" : "%1$s aktualizoval udalosť \"%2$s\"",
     "Invitation: %1$s" : "Pozvánka: %1$s",
+    "%1$s would like to invite you to \"%2$s\"" : "%1$s by vás chcel/a pozvať na \"%2$s\"",
     "Organizer:" : "Organizátor:",
     "Attendees:" : "Účastníci:",
     "Title:" : "Názov:",
@@ -109,6 +116,8 @@
     "{actor} updated contact {card} in address book {addressbook}" : "{actor} upravil kontakt {card} v adresári {addressbook}",
     "You updated contact {card} in address book {addressbook}" : "Upravili ste kontakt {card} v adresári {addressbook}",
     "A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>kontakt</strong> alebo <strong>adresár</strong> bol upravený",
+    "Accounts" : "Účty",
+    "System address book which holds all accounts" : "Systémový adresár, ktorý obsahuje všetky účty.",
     "File is not updatable: %1$s" : "Súbor nie je možné aktualizovať: %1$s",
     "Could not write to final file, canceled by hook" : "Nepodarilo sa zapísať do konečného súboru, zrušené háčikom (hook)",
     "Could not write file contents" : "Nepodarilo sa zapísať obsah súboru",
@@ -136,18 +145,30 @@
     "Completed on %s" : "Dokončené %s",
     "Due on %s by %s" : "Termín od %s do %s",
     "Due on %s" : "Termín do %s",
+    "DAV system address book" : "Systémový DAV adresár",
+    "No outstanding DAV system address book sync." : "Žiadna zostávajúca synchronizácia adresára systému DAV.",
+    "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV synchronizácia systémového adresára ešte nebola spustená, pretože vaša inštancia má viac ako 1000 užívateľov alebo sa vyskytla chyba. Prosím, spustite ju manuálne volaním \"occ dav:sync-system-addressbook\".",
     "Migrated calendar (%1$s)" : "Migrovaný kalendár (%1$s)",
     "Calendars including events, details and attendees" : "Kalendáre vrátane udalostí, podrobností a účastníkov",
     "Contacts and groups" : "Kontakty a skupiny",
     "WebDAV" : "WebDAV",
     "WebDAV endpoint" : "Koncový bod WebDAV",
     "First day" : "Prvý deň",
+    "Last day (inclusive)" : "Posledný deň (vrátane)",
+    "Short absence status" : "Status pre Krátku neprítomnosť",
+    "Long absence Message" : "Sprava pri Dlhej neprítomnosti",
     "Save" : "Uložiť",
+    "Disable absence" : "Zakázať neprítomnosť",
+    "Absence saved" : "Neprítomnosť uložená",
+    "Failed to save your absence settings" : "Nepodarilo sa uložiť vaše nastavenia neprítomnosti.",
+    "Absence cleared" : "Neprítomnosť odstránená",
+    "Failed to clear your absence settings" : "Nepodarilo sa vymazať vaše nastavenia neprítomnosti.",
     "Time zone:" : "Časová zóna:",
     "to" : "do",
     "Delete slot" : "Odstrániť slot",
     "No working hours set" : "Nenastavená pracovná doba",
     "Add slot" : "Pridať slot",
+    "Weekdays" : "Pracovné dni",
     "Monday" : "Pondelok",
     "Tuesday" : "Utorok",
     "Wednesday" : "Streda",
@@ -155,11 +176,16 @@
     "Friday" : "Piatok",
     "Saturday" : "Sobota",
     "Sunday" : "Nedeľa",
+    "Pick a start time for {dayName}" : "Vyberte začiatočný čas pre {dayName}",
+    "Pick a end time for {dayName}" : "Vyberte koncový čas pre {dayName}",
     "Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Automaticky nastaviť stav používateľa na „Nerušiť“ ak nie ste dostupný,  pre stlmenie všetkých upozornení.",
     "Failed to load availability" : "Nepodarilo sa načítať dostupnosť",
     "Saved availability" : "Dostupnosť bola uložená",
     "Failed to save availability" : "Nepodarilo sa uložiť dostupnosť",
     "Availability" : "Dostupnosť",
+    "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Ak nakonfigurujete svoj pracovný čas, ostatní užívatelia vás uvidia ako neprítomného, keď si rezervujete schôdzku",
+    "Absence" : "Neprítomnosť",
+    "Configure your next absence period." : "Nastavte si ďalšie obdobie svojej neprítomnosti.",
     "Calendar server" : "Kalendárový server",
     "Send invitations to attendees" : "Odoslanie pozvánok účastníkom",
     "Automatically generate a birthday calendar" : "Automaticky generovať narodeninový kalendár",

+ 1 - 1
apps/dav/l10n/uk.js

@@ -151,7 +151,7 @@ OC.L10N.register(
     "No outstanding DAV system address book sync." : "Немає незавершеної синхронізації системної адресної книги DAV.",
     "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронізація системної адресної книги DAV ще не запускалася, оскільки, або ваша система вже має понад 1000 користувачів, або сталася помилка. Будь ласка, запустіть синхронізацію вручну за допомогою команди \"occ dav:sync-system-addressbook\".",
     "Migrated calendar (%1$s)" : "Перенесений календар (%1$s)",
-    "Calendars including events, details and attendees" : "Календарі, включаючи події, деталі та відвідувачів",
+    "Calendars including events, details and attendees" : "Календарі, включно з подіями, деталями та відвідувачами",
     "Contacts and groups" : "Контакти та групи",
     "WebDAV" : "WebDAV",
     "WebDAV endpoint" : "Точка доступу WebDAV",

+ 1 - 1
apps/dav/l10n/uk.json

@@ -149,7 +149,7 @@
     "No outstanding DAV system address book sync." : "Немає незавершеної синхронізації системної адресної книги DAV.",
     "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронізація системної адресної книги DAV ще не запускалася, оскільки, або ваша система вже має понад 1000 користувачів, або сталася помилка. Будь ласка, запустіть синхронізацію вручну за допомогою команди \"occ dav:sync-system-addressbook\".",
     "Migrated calendar (%1$s)" : "Перенесений календар (%1$s)",
-    "Calendars including events, details and attendees" : "Календарі, включаючи події, деталі та відвідувачів",
+    "Calendars including events, details and attendees" : "Календарі, включно з подіями, деталями та відвідувачами",
     "Contacts and groups" : "Контакти та групи",
     "WebDAV" : "WebDAV",
     "WebDAV endpoint" : "Точка доступу WebDAV",

+ 1 - 0
apps/dav/l10n/zh_CN.js

@@ -170,6 +170,7 @@ OC.L10N.register(
     "Delete slot" : "删除插槽",
     "No working hours set" : "尚未设置工作时间",
     "Add slot" : "添加插槽",
+    "Weekdays" : "工作日",
     "Monday" : "周一",
     "Tuesday" : "周二",
     "Wednesday" : "周三",

+ 1 - 0
apps/dav/l10n/zh_CN.json

@@ -168,6 +168,7 @@
     "Delete slot" : "删除插槽",
     "No working hours set" : "尚未设置工作时间",
     "Add slot" : "添加插槽",
+    "Weekdays" : "工作日",
     "Monday" : "周一",
     "Tuesday" : "周二",
     "Wednesday" : "周三",

+ 1 - 1
apps/dav/lib/BulkUpload/BulkUploadPlugin.php

@@ -91,7 +91,7 @@ class BulkUploadPlugin extends ServerPlugin {
 
 				$node = $this->userFolder->newFile($headers['x-file-path'], $content);
 				$node->touch($mtime);
-				$node = $this->userFolder->getById($node->getId())[0];
+				$node = $this->userFolder->getFirstNodeById($node->getId());
 
 				$writtenFiles[$headers['x-file-path']] = [
 					"error" => false,

+ 102 - 56
apps/dav/lib/CalDAV/CalDavBackend.php

@@ -98,6 +98,7 @@ use Sabre\VObject\Property;
 use Sabre\VObject\Reader;
 use Sabre\VObject\Recur\EventIterator;
 use function array_column;
+use function array_map;
 use function array_merge;
 use function array_values;
 use function explode;
@@ -848,24 +849,24 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 	 * @return void
 	 */
 	public function updateCalendar($calendarId, PropPatch $propPatch) {
-		$this->atomic(function () use ($calendarId, $propPatch) {
-			$supportedProperties = array_keys($this->propertyMap);
-			$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
-
-			$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
-				$newValues = [];
-				foreach ($mutations as $propertyName => $propertyValue) {
-					switch ($propertyName) {
-						case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
-							$fieldName = 'transparent';
-							$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
-							break;
-						default:
-							$fieldName = $this->propertyMap[$propertyName][0];
-							$newValues[$fieldName] = $propertyValue;
-							break;
-					}
+		$supportedProperties = array_keys($this->propertyMap);
+		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
+
+		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
+			$newValues = [];
+			foreach ($mutations as $propertyName => $propertyValue) {
+				switch ($propertyName) {
+					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
+						$fieldName = 'transparent';
+						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
+						break;
+					default:
+						$fieldName = $this->propertyMap[$propertyName][0];
+						$newValues[$fieldName] = $propertyValue;
+						break;
 				}
+			}
+			[$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) {
 				$query = $this->db->getQueryBuilder();
 				$query->update('calendars');
 				foreach ($newValues as $fieldName => $value) {
@@ -874,15 +875,17 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 				$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
 				$query->executeStatement();
 
-				$this->addChange($calendarId, "", 2);
+				$this->addChanges($calendarId, [""], 2);
 
 				$calendarData = $this->getCalendarById($calendarId);
 				$shares = $this->getShares($calendarId);
-				$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations));
+				return [$calendarData, $shares];
+			}, $this->db);
 
-				return true;
-			});
-		}, $this->db);
+			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations));
+
+			return true;
+		});
 	}
 
 	/**
@@ -1293,7 +1296,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 				->executeStatement();
 
 			$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
-			$this->addChange($calendarId, $objectUri, 1, $calendarType);
+			$this->addChanges($calendarId, [$objectUri], 1, $calendarType);
 
 			$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
 			assert($objectRow !== null);
@@ -1354,7 +1357,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 				->executeStatement();
 
 			$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
-			$this->addChange($calendarId, $objectUri, 2, $calendarType);
+			$this->addChanges($calendarId, [$objectUri], 2, $calendarType);
 
 			$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
 			if (is_array($objectRow)) {
@@ -1404,8 +1407,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 			$this->purgeProperties($sourceCalendarId, $objectId);
 			$this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
 
-			$this->addChange($sourceCalendarId, $object['uri'], 3, $calendarType);
-			$this->addChange($targetCalendarId, $object['uri'], 1, $calendarType);
+			$this->addChanges($sourceCalendarId, [$object['uri']], 3, $calendarType);
+			$this->addChanges($targetCalendarId, [$object['uri']], 1, $calendarType);
 
 			$object = $this->getCalendarObjectById($newPrincipalUri, $objectId);
 			// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
@@ -1532,7 +1535,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 				}
 			}
 
-			$this->addChange($calendarId, $objectUri, 3, $calendarType);
+			$this->addChanges($calendarId, [$objectUri], 3, $calendarType);
 		}, $this->db);
 	}
 
@@ -1574,7 +1577,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 				// Welp, this should possibly not have happened, but let's ignore
 				return;
 			}
-			$this->addChange($row['calendarid'], $row['uri'], 1, (int) $row['calendartype']);
+			$this->addChanges($row['calendarid'], [$row['uri']], 1, (int) $row['calendartype']);
 
 			$calendarRow = $this->getCalendarById((int) $row['calendarid']);
 			if ($calendarRow === null) {
@@ -2570,22 +2573,22 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 	 * @return void
 	 */
 	public function updateSubscription($subscriptionId, PropPatch $propPatch) {
-		$this->atomic(function () use ($subscriptionId, $propPatch) {
-			$supportedProperties = array_keys($this->subscriptionPropertyMap);
-			$supportedProperties[] = '{http://calendarserver.org/ns/}source';
-
-			$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
-				$newValues = [];
-
-				foreach ($mutations as $propertyName => $propertyValue) {
-					if ($propertyName === '{http://calendarserver.org/ns/}source') {
-						$newValues['source'] = $propertyValue->getHref();
-					} else {
-						$fieldName = $this->subscriptionPropertyMap[$propertyName][0];
-						$newValues[$fieldName] = $propertyValue;
-					}
+		$supportedProperties = array_keys($this->subscriptionPropertyMap);
+		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
+
+		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
+			$newValues = [];
+
+			foreach ($mutations as $propertyName => $propertyValue) {
+				if ($propertyName === '{http://calendarserver.org/ns/}source') {
+					$newValues['source'] = $propertyValue->getHref();
+				} else {
+					$fieldName = $this->subscriptionPropertyMap[$propertyName][0];
+					$newValues[$fieldName] = $propertyValue;
 				}
+			}
 
+			$subscriptionRow = $this->atomic(function () use ($subscriptionId, $newValues) {
 				$query = $this->db->getQueryBuilder();
 				$query->update('calendarsubscriptions')
 					->set('lastmodified', $query->createNamedParameter(time()));
@@ -2595,12 +2598,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 				$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
 					->executeStatement();
 
-				$subscriptionRow = $this->getSubscriptionById($subscriptionId);
-				$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
+				return $this->getSubscriptionById($subscriptionId);
+			}, $this->db);
 
-				return true;
-			});
-		}, $this->db);
+			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
+
+			return true;
+		});
 	}
 
 	/**
@@ -2755,16 +2759,16 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 	 * Adds a change record to the calendarchanges table.
 	 *
 	 * @param mixed $calendarId
-	 * @param string $objectUri
+	 * @param string[] $objectUris
 	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
 	 * @param int $calendarType
 	 * @return void
 	 */
-	protected function addChange(int $calendarId, string $objectUri, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
+	protected function addChanges(int $calendarId, array $objectUris, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
 		$this->cachedObjects = [];
 		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
 
-		$this->atomic(function () use ($calendarId, $objectUri, $operation, $calendarType, $table) {
+		$this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table) {
 			$query = $this->db->getQueryBuilder();
 			$query->select('synctoken')
 				->from($table)
@@ -2776,13 +2780,16 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 			$query = $this->db->getQueryBuilder();
 			$query->insert('calendarchanges')
 				->values([
-					'uri' => $query->createNamedParameter($objectUri),
+					'uri' => $query->createParameter('uri'),
 					'synctoken' => $query->createNamedParameter($syncToken),
 					'calendarid' => $query->createNamedParameter($calendarId),
 					'operation' => $query->createNamedParameter($operation),
 					'calendartype' => $query->createNamedParameter($calendarType),
-				])
-				->executeStatement();
+				]);
+			foreach ($objectUris as $uri) {
+				$query->setParameter('uri', $uri);
+				$query->executeStatement();
+			}
 
 			$query = $this->db->getQueryBuilder();
 			$query->update($table)
@@ -2792,6 +2799,47 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 		}, $this->db);
 	}
 
+	public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
+		$this->cachedObjects = [];
+
+		$this->atomic(function () use ($calendarId, $calendarType) {
+			$qbAdded = $this->db->getQueryBuilder();
+			$qbAdded->select('uri')
+				->from('calendarobjects')
+				->where(
+					$qbAdded->expr()->andX(
+						$qbAdded->expr()->eq('calendarid', $qbAdded->createNamedParameter($calendarId)),
+						$qbAdded->expr()->eq('calendartype', $qbAdded->createNamedParameter($calendarType)),
+						$qbAdded->expr()->isNull('deleted_at'),
+					)
+				);
+			$resultAdded = $qbAdded->executeQuery();
+			$addedUris = $resultAdded->fetchAll(\PDO::FETCH_COLUMN);
+			$resultAdded->closeCursor();
+			// Track everything as changed
+			// Tracking the creation is not necessary because \OCA\DAV\CalDAV\CalDavBackend::getChangesForCalendar
+			// only returns the last change per object.
+			$this->addChanges($calendarId, $addedUris, 2, $calendarType);
+
+			$qbDeleted = $this->db->getQueryBuilder();
+			$qbDeleted->select('uri')
+				->from('calendarobjects')
+				->where(
+					$qbDeleted->expr()->andX(
+						$qbDeleted->expr()->eq('calendarid', $qbDeleted->createNamedParameter($calendarId)),
+						$qbDeleted->expr()->eq('calendartype', $qbDeleted->createNamedParameter($calendarType)),
+						$qbDeleted->expr()->isNotNull('deleted_at'),
+					)
+				);
+			$resultDeleted = $qbDeleted->executeQuery();
+			$deletedUris = array_map(function (string $uri) {
+				return str_replace("-deleted.ics", ".ics", $uri);
+			}, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN));
+			$resultDeleted->closeCursor();
+			$this->addChanges($calendarId, $deletedUris, 3, $calendarType);
+		}, $this->db);
+	}
+
 	/**
 	 * Parses some information from calendar objects, used for optimized
 	 * calendar-queries.
@@ -3134,9 +3182,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
 				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
 				->executeStatement();
 
-			foreach ($uris as $uri) {
-				$this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
-			}
+			$this->addChanges($subscriptionId, $uris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
 		}, $this->db);
 	}
 

+ 7 - 0
apps/dav/lib/CalDAV/Schedule/Plugin.php

@@ -95,6 +95,13 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
 		$server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90);
 		$server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']);
 		$server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']);
+
+		// We allow mutating the default calendar URL through the CustomPropertiesBackend
+		// (oc_properties table)
+		$server->protectedProperties = array_filter(
+			$server->protectedProperties,
+			static fn (string $property) => $property !== self::SCHEDULE_DEFAULT_CALENDAR_URL,
+		);
 	}
 
 	/**

+ 22 - 20
apps/dav/lib/CardDAV/CardDavBackend.php

@@ -331,24 +331,24 @@ class CardDavBackend implements BackendInterface, SyncSupport {
 	 * @return void
 	 */
 	public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
-		$this->atomic(function () use ($addressBookId, $propPatch) {
-			$supportedProperties = [
-				'{DAV:}displayname',
-				'{' . Plugin::NS_CARDDAV . '}addressbook-description',
-			];
+		$supportedProperties = [
+			'{DAV:}displayname',
+			'{' . Plugin::NS_CARDDAV . '}addressbook-description',
+		];
 
-			$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
-				$updates = [];
-				foreach ($mutations as $property => $newValue) {
-					switch ($property) {
-						case '{DAV:}displayname':
-							$updates['displayname'] = $newValue;
-							break;
-						case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
-							$updates['description'] = $newValue;
-							break;
-					}
+		$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
+			$updates = [];
+			foreach ($mutations as $property => $newValue) {
+				switch ($property) {
+					case '{DAV:}displayname':
+						$updates['displayname'] = $newValue;
+						break;
+					case '{' . Plugin::NS_CARDDAV . '}addressbook-description':
+						$updates['description'] = $newValue;
+						break;
 				}
+			}
+			[$addressBookRow, $shares] = $this->atomic(function () use ($addressBookId, $updates) {
 				$query = $this->db->getQueryBuilder();
 				$query->update('addressbooks');
 
@@ -362,11 +362,13 @@ class CardDavBackend implements BackendInterface, SyncSupport {
 
 				$addressBookRow = $this->getAddressBookById((int)$addressBookId);
 				$shares = $this->getShares((int)$addressBookId);
-				$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
+				return [$addressBookRow, $shares];
+			}, $this->db);
 
-				return true;
-			});
-		}, $this->db);
+			$this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations));
+
+			return true;
+		});
 	}
 
 	/**

+ 86 - 0
apps/dav/lib/Command/FixCalendarSyncCommand.php

@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2024 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2024 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\ProgressBar;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class FixCalendarSyncCommand extends Command {
+
+	public function __construct(private IUserManager $userManager,
+		private CalDavBackend $calDavBackend) {
+		parent::__construct('dav:fix-missing-caldav-changes');
+	}
+
+	protected function configure(): void {
+		$this->setDescription('Insert missing calendarchanges rows for existing events');
+		$this->addArgument(
+			'user',
+			InputArgument::OPTIONAL,
+			'User to fix calendar sync tokens for, if omitted all users will be fixed',
+			null,
+		);
+	}
+
+	public function execute(InputInterface $input, OutputInterface $output): int {
+		$userArg = $input->getArgument('user');
+		if ($userArg !== null) {
+			$user = $this->userManager->get($userArg);
+			if ($user === null) {
+				$output->writeln("<error>User $userArg does not exist</error>");
+				return 1;
+			}
+
+			$this->fixUserCalendars($user);
+		} else {
+			$progress = new ProgressBar($output);
+			$this->userManager->callForSeenUsers(function (IUser $user) use ($progress) {
+				$this->fixUserCalendars($user, $progress);
+			});
+			$progress->finish();
+		}
+		return 0;
+	}
+
+	private function fixUserCalendars(IUser $user, ?ProgressBar $progress = null): void {
+		$calendars = $this->calDavBackend->getCalendarsForUser("principals/users/" . $user->getUID());
+
+		foreach ($calendars as $calendar) {
+			$this->calDavBackend->restoreChanges($calendar['id']);
+		}
+
+		if ($progress !== null) {
+			$progress->advance();
+		}
+	}
+
+}

+ 2 - 6
apps/dav/lib/Connector/Sabre/FilesPlugin.php

@@ -611,14 +611,10 @@ class FilesPlugin extends ServerPlugin {
 							$metadata->setArray($metadataKey, $value);
 							break;
 						case IMetadataValueWrapper::TYPE_STRING_LIST:
-							$metadata->setStringList(
-								$metadataKey, $value, $knownMetadata->isIndex($metadataKey)
-							);
+							$metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
 							break;
 						case IMetadataValueWrapper::TYPE_INT_LIST:
-							$metadata->setIntList(
-								$metadataKey, $value, $knownMetadata->isIndex($metadataKey)
-							);
+							$metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
 							break;
 					}
 

+ 3 - 3
apps/dav/lib/Connector/Sabre/FilesReportPlugin.php

@@ -424,14 +424,14 @@ class FilesReportPlugin extends ServerPlugin {
 		}
 		$folder = $this->userFolder;
 		if (trim($rootNode->getPath(), '/') !== '') {
+			/** @var Folder $folder */
 			$folder = $folder->get($rootNode->getPath());
 		}
 
 		$results = [];
 		foreach ($fileIds as $fileId) {
-			$entry = $folder->getById($fileId);
+			$entry = $folder->getFirstNodeById($fileId);
 			if ($entry) {
-				$entry = current($entry);
 				$results[] = $this->wrapNode($entry);
 			}
 		}
@@ -439,7 +439,7 @@ class FilesReportPlugin extends ServerPlugin {
 		return $results;
 	}
 
-	protected function wrapNode(\OCP\Files\File|\OCP\Files\Folder $node): File|Directory {
+	protected function wrapNode(\OCP\Files\Node $node): File|Directory {
 		if ($node instanceof \OCP\Files\File) {
 			return new File($this->fileView, $node);
 		} else {

+ 1 - 0
apps/dav/lib/Connector/Sabre/Principal.php

@@ -260,6 +260,7 @@ class Principal implements BackendInterface {
 	 * @return int
 	 */
 	public function updatePrincipal($path, PropPatch $propPatch) {
+		// Updating schedule-default-calendar-URL is handled in CustomPropertiesBackend
 		return 0;
 	}
 

+ 1 - 0
apps/dav/lib/Connector/Sabre/ServerFactory.php

@@ -188,6 +188,7 @@ class ServerFactory {
 				$server->addPlugin(
 					new \Sabre\DAV\PropertyStorage\Plugin(
 						new \OCA\DAV\DAV\CustomPropertiesBackend(
+							$server,
 							$objectTree,
 							$this->databaseConnection,
 							$this->userSession->getUser()

+ 2 - 3
apps/dav/lib/Controller/DirectController.php

@@ -104,9 +104,9 @@ class DirectController extends OCSController {
 	public function getUrl(int $fileId, int $expirationTime = 60 * 60 * 8): DataResponse {
 		$userFolder = $this->rootFolder->getUserFolder($this->userId);
 
-		$files = $userFolder->getById($fileId);
+		$file = $userFolder->getFirstNodeById($fileId);
 
-		if ($files === []) {
+		if (!$file) {
 			throw new OCSNotFoundException();
 		}
 
@@ -114,7 +114,6 @@ class DirectController extends OCSController {
 			throw new OCSBadRequestException('Expiration time should be greater than 0 and less than or equal to ' . (60 * 60 * 24));
 		}
 
-		$file = array_shift($files);
 		if (!($file instanceof File)) {
 			throw new OCSBadRequestException('Direct download only works for files');
 		}

+ 135 - 4
apps/dav/lib/DAV/CustomPropertiesBackend.php

@@ -6,6 +6,7 @@
  * @author Georg Ehrke <oc.list@georgehrke.com>
  * @author Robin Appelman <robin@icewind.nl>
  * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license AGPL-3.0
  *
@@ -31,11 +32,19 @@ use OCA\DAV\Connector\Sabre\FilesPlugin;
 use OCP\DB\QueryBuilder\IQueryBuilder;
 use OCP\IDBConnection;
 use OCP\IUser;
+use Sabre\CalDAV\ICalendar;
+use Sabre\DAV\Exception as DavException;
 use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
 use Sabre\DAV\PropFind;
 use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
 use Sabre\DAV\Tree;
 use Sabre\DAV\Xml\Property\Complex;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\DAV\Xml\Property\LocalHref;
+use Sabre\Xml\ParseException;
+use Sabre\Xml\Service as XmlService;
+
 use function array_intersect;
 
 class CustomPropertiesBackend implements BackendInterface {
@@ -58,6 +67,11 @@ class CustomPropertiesBackend implements BackendInterface {
 	 */
 	public const PROPERTY_TYPE_OBJECT = 3;
 
+	/**
+	 * Value is stored as a {DAV:}href string.
+	 */
+	public const PROPERTY_TYPE_HREF = 4;
+
 	/**
 	 * Ignored properties
 	 *
@@ -105,6 +119,15 @@ class CustomPropertiesBackend implements BackendInterface {
 	 */
 	private const PUBLISHED_READ_ONLY_PROPERTIES = [
 		'{urn:ietf:params:xml:ns:caldav}calendar-availability',
+		'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
+	];
+
+	/**
+	 * Map of custom XML elements to parse when trying to deserialize an instance of
+	 * \Sabre\DAV\Xml\Property\Complex to find a more specialized PROPERTY_TYPE_*
+	 */
+	private const COMPLEX_XML_ELEMENT_MAP = [
+		'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => Href::class,
 	];
 
 	/**
@@ -129,19 +152,29 @@ class CustomPropertiesBackend implements BackendInterface {
 	 */
 	private $userCache = [];
 
+	private Server $server;
+	private XmlService $xmlService;
+
 	/**
 	 * @param Tree $tree node tree
 	 * @param IDBConnection $connection database connection
 	 * @param IUser $user owner of the tree and properties
 	 */
 	public function __construct(
+		Server $server,
 		Tree $tree,
 		IDBConnection $connection,
 		IUser $user,
 	) {
+		$this->server = $server;
 		$this->tree = $tree;
 		$this->connection = $connection;
 		$this->user = $user;
+		$this->xmlService = new XmlService();
+		$this->xmlService->elementMap = array_merge(
+			$this->xmlService->elementMap,
+			self::COMPLEX_XML_ELEMENT_MAP,
+		);
 	}
 
 	/**
@@ -199,6 +232,21 @@ class CustomPropertiesBackend implements BackendInterface {
 			}
 		}
 
+		// substr of principals/users/ => path is a user principal
+		// two '/' => this a principal collection (and not some child object)
+		if (str_starts_with($path, 'principals/users/') && substr_count($path, '/') === 2) {
+			$allRequestedProps = $propFind->getRequestedProperties();
+			$customProperties = [
+				'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
+			];
+
+			foreach ($customProperties as $customProperty) {
+				if (in_array($customProperty, $allRequestedProps, true)) {
+					$requestedProps[] = $customProperty;
+				}
+			}
+		}
+
 		if (empty($requestedProps)) {
 			return;
 		}
@@ -211,9 +259,19 @@ class CustomPropertiesBackend implements BackendInterface {
 		// First fetch the published properties (set by another user), then get the ones set by
 		// the current user. If both are set then the latter as priority.
 		foreach ($this->getPublishedProperties($path, $requestedProps) as $propName => $propValue) {
+			try {
+				$this->validateProperty($path, $propName, $propValue);
+			} catch (DavException $e) {
+				continue;
+			}
 			$propFind->set($propName, $propValue);
 		}
 		foreach ($this->getUserProperties($path, $requestedProps) as $propName => $propValue) {
+			try {
+				$this->validateProperty($path, $propName, $propValue);
+			} catch (DavException $e) {
+				continue;
+			}
 			$propFind->set($propName, $propValue);
 		}
 	}
@@ -264,6 +322,30 @@ class CustomPropertiesBackend implements BackendInterface {
 		$statement->closeCursor();
 	}
 
+	/**
+	 * Validate the value of a property. Will throw if a value is invalid.
+	 *
+	 * @throws DavException The value of the property is invalid
+	 */
+	private function validateProperty(string $path, string $propName, mixed $propValue): void {
+		switch ($propName) {
+			case '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL':
+				/** @var Href $propValue */
+				$href = $propValue->getHref();
+				if ($href === null) {
+					throw new DavException('Href is empty');
+				}
+
+				// $path is the principal here as this prop is only set on principals
+				$node = $this->tree->getNodeForPath($href);
+				if (!($node instanceof ICalendar) || $node->getOwner() !== $path) {
+					throw new DavException('No such calendar');
+				}
+
+				break;
+		}
+	}
+
 	/**
 	 * @param string $path
 	 * @param string[] $requestedProperties
@@ -393,7 +475,11 @@ class CustomPropertiesBackend implements BackendInterface {
 							->executeStatement();
 					}
 				} else {
-					[$value, $valueType] = $this->encodeValueForDatabase($propertyValue);
+					[$value, $valueType] = $this->encodeValueForDatabase(
+						$path,
+						$propertyName,
+						$propertyValue,
+					);
 					$dbParameters['propertyValue'] = $value;
 					$dbParameters['valueType'] = $valueType;
 
@@ -436,15 +522,38 @@ class CustomPropertiesBackend implements BackendInterface {
 	}
 
 	/**
-	 * @param mixed $value
-	 * @return array
+	 * @throws ParseException If parsing a \Sabre\DAV\Xml\Property\Complex value fails
+	 * @throws DavException If the property value is invalid
 	 */
-	private function encodeValueForDatabase($value): array {
+	private function encodeValueForDatabase(string $path, string $name, mixed $value): array {
+		// Try to parse a more specialized property type first
+		if ($value instanceof Complex) {
+			$xml = $this->xmlService->write($name, [$value], $this->server->getBaseUri());
+			$value = $this->xmlService->parse($xml, $this->server->getBaseUri()) ?? $value;
+		}
+
+		if ($name === '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL') {
+			$value = $this->encodeDefaultCalendarUrl($value);
+		}
+
+		try {
+			$this->validateProperty($path, $name, $value);
+		} catch (DavException $e) {
+			throw new DavException(
+				"Property \"$name\" has an invalid value: " . $e->getMessage(),
+				0,
+				$e,
+			);
+		}
+
 		if (is_scalar($value)) {
 			$valueType = self::PROPERTY_TYPE_STRING;
 		} elseif ($value instanceof Complex) {
 			$valueType = self::PROPERTY_TYPE_XML;
 			$value = $value->getXml();
+		} elseif ($value instanceof Href) {
+			$valueType = self::PROPERTY_TYPE_HREF;
+			$value = $value->getHref();
 		} else {
 			$valueType = self::PROPERTY_TYPE_OBJECT;
 			$value = serialize($value);
@@ -459,6 +568,8 @@ class CustomPropertiesBackend implements BackendInterface {
 		switch ($valueType) {
 			case self::PROPERTY_TYPE_XML:
 				return new Complex($value);
+			case self::PROPERTY_TYPE_HREF:
+				return new Href($value);
 			case self::PROPERTY_TYPE_OBJECT:
 				return unserialize($value);
 			case self::PROPERTY_TYPE_STRING:
@@ -467,6 +578,26 @@ class CustomPropertiesBackend implements BackendInterface {
 		}
 	}
 
+	private function encodeDefaultCalendarUrl(Href $value): Href {
+		$href = $value->getHref();
+		if ($href === null) {
+			return $value;
+		}
+
+		if (!str_starts_with($href, '/')) {
+			return $value;
+		}
+
+		try {
+			// Build path relative to the dav base URI to be used later to find the node
+			$value = new LocalHref($this->server->calculateUri($href) . '/');
+		} catch (DavException\Forbidden) {
+			// Not existing calendars will be handled later when the value is validated
+		}
+
+		return $value;
+	}
+
 	private function createDeleteQuery(): IQueryBuilder {
 		$deleteQuery = $this->connection->getQueryBuilder();
 		$deleteQuery->delete('properties')

+ 6 - 3
apps/dav/lib/Direct/DirectFile.php

@@ -108,13 +108,16 @@ class DirectFile implements IFile {
 	private function getFile() {
 		if ($this->file === null) {
 			$userFolder = $this->rootFolder->getUserFolder($this->direct->getUserId());
-			$files = $userFolder->getById($this->direct->getFileId());
+			$file = $userFolder->getFirstNodeById($this->direct->getFileId());
 
-			if ($files === []) {
+			if (!$file) {
 				throw new NotFound();
 			}
+			if (!$file instanceof File) {
+				throw new Forbidden("direct download not allowed on directories");
+			}
 
-			$this->file = array_shift($files);
+			$this->file = $file;
 		}
 
 		return $this->file;

+ 2 - 1
apps/dav/lib/RootCollection.php

@@ -134,7 +134,8 @@ class RootCollection extends SimpleCollection {
 			\OC::$server->getSystemTagObjectMapper(),
 			\OC::$server->getUserSession(),
 			$groupManager,
-			$dispatcher
+			$dispatcher,
+			$rootFolder,
 		);
 		$systemTagInUseCollection = \OCP\Server::get(SystemTag\SystemTagsInUseCollection::class);
 		$commentsCollection = new Comments\RootCollection(

+ 1 - 0
apps/dav/lib/Server.php

@@ -276,6 +276,7 @@ class Server {
 				$this->server->addPlugin(
 					new \Sabre\DAV\PropertyStorage\Plugin(
 						new CustomPropertiesBackend(
+							$this->server,
 							$this->server->tree,
 							\OC::$server->getDatabaseConnection(),
 							\OC::$server->getUserSession()->getUser()

+ 10 - 3
apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php

@@ -27,6 +27,7 @@
 namespace OCA\DAV\SystemTag;
 
 use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IRootFolder;
 use OCP\IGroupManager;
 use OCP\IUserSession;
 use OCP\SystemTag\ISystemTagManager;
@@ -42,6 +43,7 @@ class SystemTagsRelationsCollection extends SimpleCollection {
 		IUserSession $userSession,
 		IGroupManager $groupManager,
 		IEventDispatcher $dispatcher,
+		IRootFolder $rootFolder,
 	) {
 		$children = [
 			new SystemTagsObjectTypeCollection(
@@ -50,9 +52,14 @@ class SystemTagsRelationsCollection extends SimpleCollection {
 				$tagMapper,
 				$userSession,
 				$groupManager,
-				function ($name) {
-					$nodes = \OC::$server->getUserFolder()->getById((int)$name);
-					return !empty($nodes);
+				function (string $name) use ($rootFolder, $userSession): bool {
+					$user = $userSession->getUser();
+					if ($user) {
+						$node = $rootFolder->getUserFolder($user->getUID())->getFirstNodeById((int)$name);
+						return $node !== null;
+					} else {
+						return false;
+					}
 				}
 			),
 		];

+ 1 - 1
apps/dav/lib/Upload/ChunkingV2Plugin.php

@@ -344,7 +344,7 @@ class ChunkingV2Plugin extends ServerPlugin {
 
 		// If the file was not uploaded to the user storage directly we need to copy/move it
 		try {
-			$uploadFileAbsolutePath = Filesystem::getRoot() . $uploadFile->getPath();
+			$uploadFileAbsolutePath = $uploadFile->getFileInfo()->getPath();
 			if ($uploadFileAbsolutePath !== $targetAbsolutePath) {
 				$uploadFile = $rootFolder->get($uploadFile->getFileInfo()->getPath());
 				if ($exists) {

+ 19 - 11
apps/dav/src/dav/client.js

@@ -19,21 +19,29 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-import * as webdav from 'webdav'
-import axios from '@nextcloud/axios'
+import { createClient } from 'webdav'
 import memoize from 'lodash/fp/memoize.js'
 import { generateRemoteUrl } from '@nextcloud/router'
-import { getCurrentUser } from '@nextcloud/auth'
+import { getCurrentUser, getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
 
 export const getClient = memoize((service) => {
-	// Add this so the server knows it is an request from the browser
-	axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
+	// init webdav client
+	const remote = generateRemoteUrl(`dav/${service}/${getCurrentUser().uid}`)
+	const client = createClient(remote)
 
-	// force our axios
-	const patcher = webdav.getPatcher()
-	patcher.patch('request', axios)
+	// set CSRF token header
+	const setHeaders = (token) => {
+		client.setHeaders({
+			// Add this so the server knows it is an request from the browser
+			'X-Requested-With': 'XMLHttpRequest',
+			// Inject user auth
+			requesttoken: token ?? '',
+		})
+	}
 
-	return webdav.createClient(
-		generateRemoteUrl(`dav/${service}/${getCurrentUser().uid}`)
-	)
+	// refresh headers when request token changes
+	onRequestTokenUpdate(setHeaders)
+	setHeaders(getRequestToken())
+
+	return client;
 })

+ 5 - 3
apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php

@@ -39,6 +39,7 @@ use OCP\App\IAppManager;
 use OCP\EventDispatcher\IEventDispatcher;
 use OCP\ICacheFactory;
 use OCP\IConfig;
+use OCP\IDBConnection;
 use OCP\IGroupManager;
 use OCP\IUserManager;
 use OCP\IUserSession;
@@ -70,6 +71,7 @@ abstract class AbstractCalDavBackend extends TestCase {
 	private IConfig|MockObject $config;
 	private ISecureRandom $random;
 	protected SharingBackend $sharingBackend;
+	protected IDBConnection $db;
 	public const UNIT_TEST_USER = 'principals/users/caldav-unit-test';
 	public const UNIT_TEST_USER1 = 'principals/users/caldav-unit-test1';
 	public const UNIT_TEST_GROUP = 'principals/groups/caldav-unit-test-group';
@@ -105,7 +107,7 @@ abstract class AbstractCalDavBackend extends TestCase {
 			->withAnyParameters()
 			->willReturn([self::UNIT_TEST_GROUP, self::UNIT_TEST_GROUP2]);
 
-		$db = \OC::$server->getDatabaseConnection();
+		$this->db = \OC::$server->getDatabaseConnection();
 		$this->random = \OC::$server->getSecureRandom();
 		$this->logger = $this->createMock(LoggerInterface::class);
 		$this->config = $this->createMock(IConfig::class);
@@ -114,10 +116,10 @@ abstract class AbstractCalDavBackend extends TestCase {
 			$this->groupManager,
 			$this->principal,
 			$this->createMock(ICacheFactory::class),
-			new Service(new SharingMapper($db)),
+			new Service(new SharingMapper($this->db)),
 			$this->logger);
 		$this->backend = new CalDavBackend(
-			$db,
+			$this->db,
 			$this->principal,
 			$this->userManager,
 			$this->random,

+ 89 - 0
apps/dav/tests/unit/CalDAV/CalDavBackendTest.php

@@ -1502,4 +1502,93 @@ EOD;
 			}
 		}
 	}
+
+	public function testRestoreChanges(): void {
+		$calendarId = $this->createTestCalendar();
+		$uri1 = static::getUniqueID('calobj1') . '.ics';
+		$calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+		$this->backend->createCalendarObject($calendarId, $uri1, $calData);
+		$calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event – UPDATED
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+EOD;
+		$this->backend->updateCalendarObject($calendarId, $uri1, $calData);
+		$uri2 = static::getUniqueID('calobj2') . '.ics';
+		$calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec9
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+		$this->backend->createCalendarObject($calendarId, $uri2, $calData);
+		$changesBefore = $this->backend->getChangesForCalendar($calendarId, null, 1);
+		$this->backend->deleteCalendarObject($calendarId, $uri2);
+		$uri3 = static::getUniqueID('calobj3') . '.ics';
+		$calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:Nextcloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3e10
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+		$this->backend->createCalendarObject($calendarId, $uri3, $calData);
+		$deleteChanges = $this->db->getQueryBuilder();
+		$deleteChanges->delete('calendarchanges')
+			->where($deleteChanges->expr()->eq('calendarid', $deleteChanges->createNamedParameter($calendarId)));
+		$deleteChanges->executeStatement();
+
+		$this->backend->restoreChanges($calendarId);
+
+		$changesAfter = $this->backend->getChangesForCalendar($calendarId, $changesBefore['syncToken'], 1);
+		self::assertEquals([], $changesAfter['added']);
+		self::assertEqualsCanonicalizing([$uri1, $uri3], $changesAfter['modified']);
+		self::assertEquals([$uri2], $changesAfter['deleted']);
+	}
 }

+ 38 - 0
apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php

@@ -163,4 +163,42 @@ class RateLimitingPluginTest extends TestCase {
 		$this->plugin->beforeBind('calendars/foo/cal');
 	}
 
+	public function testNoCalendarsSubscriptsLimit(): void {
+		$user = $this->createMock(IUser::class);
+		$this->userManager->expects(self::once())
+			->method('get')
+			->with($this->userId)
+			->willReturn($user);
+		$user->method('getUID')->willReturn('user123');
+		$this->config
+			->method('getValueInt')
+			->with('dav')
+			->willReturnCallback(function ($app, $key, $default) {
+				switch ($key) {
+					case 'maximumCalendarsSubscriptions':
+						return -1;
+					default:
+						return $default;
+				}
+			});
+		$this->limiter->expects(self::once())
+			->method('registerUserRequest')
+			->with(
+				'caldav-create-calendar',
+				10,
+				3600,
+				$user,
+			);
+		$this->caldavBackend->expects(self::never())
+			->method('getCalendarsForUserCount')
+			->with('principals/users/user123')
+			->willReturn(27);
+		$this->caldavBackend->expects(self::never())
+			->method('getSubscriptionsForUserCount')
+			->with('principals/users/user123')
+			->willReturn(3);
+
+		$this->plugin->beforeBind('calendars/foo/cal');
+	}
+
 }

+ 1 - 0
apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php

@@ -87,6 +87,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
 			->willReturn($userId);
 
 		$this->plugin = new \OCA\DAV\DAV\CustomPropertiesBackend(
+			$this->server,
 			$this->tree,
 			\OC::$server->getDatabaseConnection(),
 			$this->user

+ 6 - 6
apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php

@@ -320,14 +320,14 @@ class FilesReportPluginTest extends \Test\TestCase {
 			->willReturn('/');
 
 		$this->userFolder->expects($this->exactly(2))
-			->method('getById')
+			->method('getFirstNodeById')
 			->withConsecutive(
 				['111'],
 				['222'],
 			)
 			->willReturnOnConsecutiveCalls(
-				[$filesNode1],
-				[$filesNode2],
+				$filesNode1,
+				$filesNode2,
 			);
 
 		/** @var \OCA\DAV\Connector\Sabre\Directory|MockObject $reportTargetNode */
@@ -373,14 +373,14 @@ class FilesReportPluginTest extends \Test\TestCase {
 			->willReturn($subNode);
 
 		$subNode->expects($this->exactly(2))
-			->method('getById')
+			->method('getFirstNodeById')
 			->withConsecutive(
 				['111'],
 				['222'],
 			)
 			->willReturnOnConsecutiveCalls(
-				[$filesNode1],
-				[$filesNode2],
+				$filesNode1,
+				$filesNode2,
 			);
 
 		/** @var \OCA\DAV\Connector\Sabre\Directory|MockObject $reportTargetNode */

+ 4 - 4
apps/dav/tests/unit/Controller/DirectControllerTest.php

@@ -110,9 +110,9 @@ class DirectControllerTest extends TestCase {
 
 		$folder = $this->createMock(Folder::class);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(101)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$this->expectException(OCSBadRequestException::class);
 		$this->controller->getUrl(101);
@@ -129,9 +129,9 @@ class DirectControllerTest extends TestCase {
 		$this->timeFactory->method('getTime')
 			->willReturn(42);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(101)
-			->willReturn([$file]);
+			->willReturn($file);
 
 		$userFolder->method('getRelativePath')
 			->willReturn('/path');

+ 195 - 5
apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php

@@ -8,6 +8,7 @@
  * @author Morris Jobke <hey@morrisjobke.de>
  * @author Robin Appelman <robin@icewind.nl>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -28,17 +29,29 @@
 namespace OCA\DAV\Tests\DAV;
 
 use OCA\DAV\DAV\CustomPropertiesBackend;
+use OCP\DB\QueryBuilder\IQueryBuilder;
 use OCP\IDBConnection;
 use OCP\IUser;
+use Sabre\CalDAV\ICalendar;
+use Sabre\DAV\Exception\NotFound;
 use Sabre\DAV\PropFind;
 use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
 use Sabre\DAV\Tree;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\DAVACL\IACL;
+use Sabre\DAVACL\IPrincipal;
 use Test\TestCase;
 
 /**
  * @group DB
  */
 class CustomPropertiesBackendTest extends TestCase {
+	private const BASE_URI = '/remote.php/dav/';
+
+	/** @var Server | \PHPUnit\Framework\MockObject\MockObject */
+	private $server;
+
 	/** @var Tree | \PHPUnit\Framework\MockObject\MockObject */
 	private $tree;
 
@@ -54,6 +67,9 @@ class CustomPropertiesBackendTest extends TestCase {
 	protected function setUp(): void {
 		parent::setUp();
 
+		$this->server = $this->createMock(Server::class);
+		$this->server->method('getBaseUri')
+			->willReturn(self::BASE_URI);
 		$this->tree = $this->createMock(Tree::class);
 		$this->user = $this->createMock(IUser::class);
 		$this->user->method('getUID')
@@ -62,9 +78,10 @@ class CustomPropertiesBackendTest extends TestCase {
 		$this->dbConnection = \OC::$server->getDatabaseConnection();
 
 		$this->backend = new CustomPropertiesBackend(
+			$this->server,
 			$this->tree,
 			$this->dbConnection,
-			$this->user
+			$this->user,
 		);
 	}
 
@@ -90,7 +107,13 @@ class CustomPropertiesBackendTest extends TestCase {
 		}
 	}
 
-	protected function insertProp(string $user, string $path, string $name, string $value) {
+	protected function insertProp(string $user, string $path, string $name, mixed $value) {
+		$type = CustomPropertiesBackend::PROPERTY_TYPE_STRING;
+		if ($value instanceof Href) {
+			$value = $value->getHref();
+			$type = CustomPropertiesBackend::PROPERTY_TYPE_HREF;
+		}
+
 		$query = $this->dbConnection->getQueryBuilder();
 		$query->insert('properties')
 			->values([
@@ -98,13 +121,14 @@ class CustomPropertiesBackendTest extends TestCase {
 				'propertypath' => $query->createNamedParameter($this->formatPath($path)),
 				'propertyname' => $query->createNamedParameter($name),
 				'propertyvalue' => $query->createNamedParameter($value),
+				'valuetype' => $query->createNamedParameter($type, IQueryBuilder::PARAM_INT)
 			]);
 		$query->execute();
 	}
 
 	protected function getProps(string $user, string $path) {
 		$query = $this->dbConnection->getQueryBuilder();
-		$query->select('propertyname', 'propertyvalue')
+		$query->select('propertyname', 'propertyvalue', 'valuetype')
 			->from('properties')
 			->where($query->expr()->eq('userid', $query->createNamedParameter($user)))
 			->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($this->formatPath($path))));
@@ -112,7 +136,11 @@ class CustomPropertiesBackendTest extends TestCase {
 		$result = $query->execute();
 		$data = [];
 		while ($row = $result->fetch()) {
-			$data[$row['propertyname']] = $row['propertyvalue'];
+			$value = $row['propertyvalue'];
+			if ((int)$row['valuetype'] === CustomPropertiesBackend::PROPERTY_TYPE_HREF) {
+				$value = new Href($value);
+			}
+			$data[$row['propertyname']] = $value;
 		}
 		$result->closeCursor();
 
@@ -122,9 +150,10 @@ class CustomPropertiesBackendTest extends TestCase {
 	public function testPropFindNoDbCalls(): void {
 		$db = $this->createMock(IDBConnection::class);
 		$backend = new CustomPropertiesBackend(
+			$this->server,
 			$this->tree,
 			$db,
-			$this->user
+			$this->user,
 		);
 
 		$propFind = $this->createMock(PropFind::class);
@@ -186,10 +215,169 @@ class CustomPropertiesBackendTest extends TestCase {
 		$this->assertEquals($props, $setProps);
 	}
 
+	public function testPropFindPrincipalCall(): void {
+		$this->tree->method('getNodeForPath')
+			->willReturnCallback(function ($uri) {
+				$node = $this->createMock(ICalendar::class);
+				$node->method('getOwner')
+					->willReturn('principals/users/dummy_user_42');
+				return $node;
+			});
+
+		$propFind = $this->createMock(PropFind::class);
+		$propFind->method('get404Properties')
+			->with()
+			->willReturn([
+				'{DAV:}getcontentlength',
+				'{DAV:}getcontenttype',
+				'{DAV:}getetag',
+				'{abc}def',
+			]);
+
+		$propFind->method('getRequestedProperties')
+			->with()
+			->willReturn([
+				'{DAV:}getcontentlength',
+				'{DAV:}getcontenttype',
+				'{DAV:}getetag',
+				'{abc}def',
+				'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
+			]);
+
+		$props = [
+			'{abc}def' => 'a',
+			'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/admin/personal'),
+		];
+		$this->insertProps('dummy_user_42', 'principals/users/dummy_user_42', $props);
+
+		$setProps = [];
+		$propFind->method('set')
+			->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
+				$setProps[$name] = $value;
+			});
+
+		$this->backend->propFind('principals/users/dummy_user_42', $propFind);
+		$this->assertEquals($props, $setProps);
+	}
+
+	public function propFindPrincipalScheduleDefaultCalendarProviderUrlProvider(): array {
+		// [ user, nodes, existingProps, requestedProps, returnedProps ]
+		return [
+			[ // Exists
+				'dummy_user_42',
+				['calendars/dummy_user_42/foo/' => ICalendar::class],
+				['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
+				['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+				['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
+			],
+			[ // Doesn't exist
+				'dummy_user_42',
+				['calendars/dummy_user_42/foo/' => ICalendar::class],
+				['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/bar/')],
+				['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+				[],
+			],
+			[ // No privilege
+				'dummy_user_42',
+				['calendars/user2/baz/' => ICalendar::class],
+				['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/user2/baz/')],
+				['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+				[],
+			],
+			[ // Not a calendar
+				'dummy_user_42',
+				['foo/dummy_user_42/bar/' => IACL::class],
+				['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/dummy_user_42/bar/')],
+				['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
+				[],
+			],
+		];
+
+	}
+
+	/**
+	 * @dataProvider propFindPrincipalScheduleDefaultCalendarProviderUrlProvider
+	 */
+	public function testPropFindPrincipalScheduleDefaultCalendarUrl(
+		string $user,
+		array $nodes,
+		array $existingProps,
+		array $requestedProps,
+		array $returnedProps,
+	): void {
+		$propFind = $this->createMock(PropFind::class);
+		$propFind->method('get404Properties')
+			->with()
+			->willReturn([
+				'{DAV:}getcontentlength',
+				'{DAV:}getcontenttype',
+				'{DAV:}getetag',
+			]);
+
+		$propFind->method('getRequestedProperties')
+			->with()
+			->willReturn(array_merge([
+				'{DAV:}getcontentlength',
+				'{DAV:}getcontenttype',
+				'{DAV:}getetag',
+				'{abc}def',
+			],
+				$requestedProps,
+			));
+
+		$this->server->method('calculateUri')
+			->willReturnCallback(function ($uri) {
+				if (!str_starts_with($uri, self::BASE_URI)) {
+					return trim(substr($uri, strlen(self::BASE_URI)), '/');
+				}
+				return null;
+			});
+		$this->tree->method('getNodeForPath')
+			->willReturnCallback(function ($uri) use ($nodes) {
+				if (str_starts_with($uri, 'principals/')) {
+					return $this->createMock(IPrincipal::class);
+				}
+				if (array_key_exists($uri, $nodes)) {
+					$owner = explode('/', $uri)[1];
+					$node = $this->createMock($nodes[$uri]);
+					$node->method('getOwner')
+						->willReturn("principals/users/$owner");
+					return $node;
+				}
+				throw new NotFound('Node not found');
+			});
+
+		$this->insertProps($user, "principals/users/$user", $existingProps);
+
+		$setProps = [];
+		$propFind->method('set')
+			->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
+				$setProps[$name] = $value;
+			});
+
+		$this->backend->propFind("principals/users/$user", $propFind);
+		$this->assertEquals($returnedProps, $setProps);
+	}
+
 	/**
 	 * @dataProvider propPatchProvider
 	 */
 	public function testPropPatch(string $path, array $existing, array $props, array $result): void {
+		$this->server->method('calculateUri')
+			->willReturnCallback(function ($uri) {
+				if (str_starts_with($uri, self::BASE_URI)) {
+					return trim(substr($uri, strlen(self::BASE_URI)), '/');
+				}
+				return null;
+			});
+		$this->tree->method('getNodeForPath')
+			->willReturnCallback(function ($uri) {
+				$node = $this->createMock(ICalendar::class);
+				$node->method('getOwner')
+					->willReturn('principals/users/' . $this->user->getUID());
+				return $node;
+			});
+
 		$this->insertProps($this->user->getUID(), $path, $existing);
 		$propPatch = new PropPatch($props);
 
@@ -207,6 +395,8 @@ class CustomPropertiesBackendTest extends TestCase {
 			['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
 			['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => null], []],
 			[$longPath, [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
+			['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
+			['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href(self::BASE_URI . 'foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
 		];
 	}
 

+ 2 - 2
apps/dav/tests/unit/Direct/DirectFileTest.php

@@ -73,9 +73,9 @@ class DirectFileTest extends TestCase {
 			->willReturn($this->userFolder);
 
 		$this->file = $this->createMock(File::class);
-		$this->userFolder->method('getById')
+		$this->userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$this->file]);
+			->willReturn($this->file);
 
 		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
 

+ 16 - 16
apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php

@@ -84,8 +84,8 @@ class SystemTagsObjectTypeCollectionTest extends \Test\TestCase {
 		$userFolder = $this->userFolder;
 
 		$closure = function ($name) use ($userFolder) {
-			$nodes = $userFolder->getById(intval($name));
-			return !empty($nodes);
+			$node = $userFolder->getFirstNodeById(intval($name));
+			return $node !== null;
 		};
 
 		$this->node = new \OCA\DAV\SystemTag\SystemTagsObjectTypeCollection(
@@ -98,14 +98,14 @@ class SystemTagsObjectTypeCollectionTest extends \Test\TestCase {
 		);
 	}
 
-	
+
 	public function testForbiddenCreateFile(): void {
 		$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
 
 		$this->node->createFile('555');
 	}
 
-	
+
 	public function testForbiddenCreateDirectory(): void {
 		$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
 
@@ -114,27 +114,27 @@ class SystemTagsObjectTypeCollectionTest extends \Test\TestCase {
 
 	public function testGetChild(): void {
 		$this->userFolder->expects($this->once())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with('555')
-			->willReturn([true]);
+			->willReturn($this->createMock(\OCP\Files\Node::class));
 		$childNode = $this->node->getChild('555');
 
 		$this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection', $childNode);
 		$this->assertEquals('555', $childNode->getName());
 	}
 
-	
+
 	public function testGetChildWithoutAccess(): void {
 		$this->expectException(\Sabre\DAV\Exception\NotFound::class);
 
 		$this->userFolder->expects($this->once())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with('555')
-			->willReturn([]);
+			->willReturn(null);
 		$this->node->getChild('555');
 	}
 
-	
+
 	public function testGetChildren(): void {
 		$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
 
@@ -143,28 +143,28 @@ class SystemTagsObjectTypeCollectionTest extends \Test\TestCase {
 
 	public function testChildExists(): void {
 		$this->userFolder->expects($this->once())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with('123')
-			->willReturn([true]);
+			->willReturn($this->createMock(\OCP\Files\Node::class));
 		$this->assertTrue($this->node->childExists('123'));
 	}
 
 	public function testChildExistsWithoutAccess(): void {
 		$this->userFolder->expects($this->once())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with('555')
-			->willReturn([]);
+			->willReturn(null);
 		$this->assertFalse($this->node->childExists('555'));
 	}
 
-	
+
 	public function testDelete(): void {
 		$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
 
 		$this->node->delete();
 	}
 
-	
+
 	public function testSetName(): void {
 		$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
 

+ 1 - 0
apps/encryption/l10n/es_MX.js

@@ -28,6 +28,7 @@ OC.L10N.register(
     "Bad Signature" : "Firma equivocada",
     "Missing Signature" : "Firma faltante",
     "one-time password for server-side-encryption" : "Contraseña de una-sola-vez para la encripción del lado del servidor",
+    "Encryption password" : "Contraseña de cifrado",
     "Default encryption module" : "Módulo de encripción predeterminado",
     "Default encryption module for server-side encryption" : "Modulo de encripción por defecto para encripción de lado del servidor",
     "Encryption app is enabled but your keys are not initialized, please log-out and log-in again" : "La aplicación de encripción esta habilitada pero tus llaves no han sido inicializadas, por favor sal y vuelve a entrar a tu sesión",

+ 1 - 0
apps/encryption/l10n/es_MX.json

@@ -26,6 +26,7 @@
     "Bad Signature" : "Firma equivocada",
     "Missing Signature" : "Firma faltante",
     "one-time password for server-side-encryption" : "Contraseña de una-sola-vez para la encripción del lado del servidor",
+    "Encryption password" : "Contraseña de cifrado",
     "Default encryption module" : "Módulo de encripción predeterminado",
     "Default encryption module for server-side encryption" : "Modulo de encripción por defecto para encripción de lado del servidor",
     "Encryption app is enabled but your keys are not initialized, please log-out and log-in again" : "La aplicación de encripción esta habilitada pero tus llaves no han sido inicializadas, por favor sal y vuelve a entrar a tu sesión",

Some files were not shown because too many files changed in this diff