Compare commits
297 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c909f0125f | |||
| c62fa55007 | |||
| 5f9117b939 | |||
| 57a49a50aa | |||
| ea0229dc86 | |||
| 437f380fd9 | |||
| a316ae1590 | |||
| 3e7db013c1 | |||
| 9621e451ba | |||
| e0a1c84ebb | |||
| 2c74609e07 | |||
| f474b5b3a9 | |||
| c5cd7ef9b9 | |||
| 3cc4410273 | |||
| 720ab52e07 | |||
| 5283e9a5e2 | |||
| 53b116b8a5 | |||
| 158b3ef859 | |||
| fee878308e | |||
| 8ecadfa6bf | |||
| 300964ec4b | |||
| 82fb8f8508 | |||
| c3ddd1da46 | |||
| baae99eaad | |||
| e65084adda | |||
| 970eb8299f | |||
| 86c591ba5f | |||
| 7810a9005e | |||
| 63cf3f53c3 | |||
| 6974015e39 | |||
| fd8df1003d | |||
| ae63945736 | |||
| b3b63020c9 | |||
| fd4c103e8b | |||
| 8576c8baa6 | |||
| 7fc0318475 | |||
| 006d4ee952 | |||
| c7a50c708d | |||
| a4ab5a3cdf | |||
| 1b72ddd8c8 | |||
| 2cd491f491 | |||
| 42f45030ff | |||
| 0c56605497 | |||
| 1b273b8c2c | |||
| 14f79829f3 | |||
| 4635cb4b2e | |||
| 78a175fc74 | |||
| 953d1ab962 | |||
| 2919685603 | |||
| bfa494f247 | |||
| 072d4e8cba | |||
| 7523c38e58 | |||
| 1c7e4a1ba6 | |||
| 40239518c1 | |||
| 37d4bdce00 | |||
| a01e85799c | |||
| a53e15c971 | |||
| 7304756e03 | |||
| 1950076ca1 | |||
| 498c57f648 | |||
| 3aae7ae305 | |||
| df94cceb7b | |||
| b67a43764a | |||
| 1ae3fa4003 | |||
| 0f03a892b9 | |||
| 85a6b0d0eb | |||
| 023b98c44b | |||
| c05d3fdb2e | |||
| a338772ddd | |||
| 2b4bfcc212 | |||
| 4c25654574 | |||
| 43418eea5f | |||
| a86d917907 | |||
| a79b5dea7c | |||
| 2d8f6b366a | |||
| e6275f8759 | |||
| 27599ef45d | |||
| 08bff4cf4a | |||
| 367c877b7a | |||
| f6f83430a9 | |||
| 561b590c77 | |||
| 14872c8040 | |||
| e35a8ed063 | |||
| 9bb0721d66 | |||
| 8779ae38a4 | |||
| 21233b7e17 | |||
| 38f341c179 | |||
| 536ccf144c | |||
| fb615ef9f8 | |||
| 866063c198 | |||
| 8c68f1eeb7 | |||
| 1e72620169 | |||
| 0001c22668 | |||
| cd6a53ea4d | |||
| b9da9416be | |||
| 20c6d1a7e9 | |||
| b286bca485 | |||
| 4495794a0b | |||
| 6cf1870322 | |||
| 56897b6f3c | |||
| 76696be762 | |||
| 9255afaeab | |||
| be3cf85656 | |||
| 6e9b4eb0a8 | |||
| ff318138a2 | |||
| 1a8a6bea97 | |||
| f97495bf5a | |||
| 2f1c74d43f | |||
| d4b38083d6 | |||
| d0e9e2c859 | |||
| ebd80bc142 | |||
| 0eb530ecde | |||
| 957efe0670 | |||
| 3500018b56 | |||
| a2eed985aa | |||
| 7e9fc7604a | |||
| 768b101a9d | |||
| e6bdfcd8ed | |||
| ffe10d4916 | |||
| ff1dfc6bd0 | |||
| 0827645b53 | |||
| a4d7c12326 | |||
| ca1f779258 | |||
| 195dbad119 | |||
| 0a73113b35 | |||
| a4c923f4b9 | |||
| 25e7c10a33 | |||
| 0c200fb9ef | |||
| 70db2a4c9e | |||
| 2624d2b702 | |||
| b0b8159d6a | |||
| 3dbc479239 | |||
| d7f8a106cc | |||
| d9c53ef748 | |||
| 0b354efb22 | |||
| 477bc4e3f1 | |||
| c7430d5cb8 | |||
| aa0135c223 | |||
| 1b549d4943 | |||
| 5435f3bab0 | |||
| 2ed16599e1 | |||
| 95af3305d8 | |||
| 04b1dfdf13 | |||
| 648745d524 | |||
| ae16a28758 | |||
| 99364adc1c | |||
| cd75876c1e | |||
| 1228cfd3a2 | |||
| 2d68644e11 | |||
| 349cc7b732 | |||
| 7c6f8d2a25 | |||
| 3a02d942ae | |||
| 2c0e576a7d | |||
| 0dc971189b | |||
| e3e6a3b72e | |||
| a5b7af3d74 | |||
| c446274bf0 | |||
| 7880a5c25e | |||
| cd06b2432c | |||
| 8528916bbc | |||
| 1c2b4f2a41 | |||
| cc96782177 | |||
| 598579396c | |||
| 46b98f3bcc | |||
| f471bd292f | |||
| 4c241d4f4b | |||
| 1c50329fdb | |||
| 87bad33496 | |||
| 206aac5b5b | |||
| c1a9dfd993 | |||
| 9592390070 | |||
| 24c6a1ff0b | |||
| 1c518a291d | |||
| 31899d95b9 | |||
| 5cf799bdb4 | |||
| 7c4098d8c7 | |||
| 7896575af8 | |||
| a2d4f8d3f1 | |||
| cd9f0350b0 | |||
| 25c34ce1e5 | |||
| 6af7ca0a85 | |||
| 6bd5f6af83 | |||
| 2a1e63be14 | |||
| c051a5b650 | |||
| c2dea5faf6 | |||
| 88829b3abf | |||
| c668703021 | |||
| 0e0abcbdc4 | |||
| 01a8d7748c | |||
| 66c2607365 | |||
| cfdb919c37 | |||
| 46f5b07322 | |||
| a55e61d97c | |||
| 8813df9623 | |||
| 7f0953d520 | |||
| e90e3a70fa | |||
| db5f6a4e96 | |||
| 01d2a9f76a | |||
| b3f52ebbe8 | |||
| 4783459144 | |||
| 5644f33b44 | |||
| d0fcb7e638 | |||
| 867be352f3 | |||
| e6bcc4e4e9 | |||
| 2238548278 | |||
| d46d2d1acf | |||
| 10424f248b | |||
| ed485a9731 | |||
| aba39c717a | |||
| ca1fc249ce | |||
| f068350998 | |||
| 8683a9bb4c | |||
| e167a567f2 | |||
| d99c797aac | |||
| c24f5fb256 | |||
| 9009ef1a4c | |||
| 7964f338dc | |||
| 9a16e4fd14 | |||
| 9fbb3d8b4d | |||
| b231e18867 | |||
| ecacdd2e77 | |||
| dbfbb8da7f | |||
| 4fe5c8a217 | |||
| 8b07a9be20 | |||
| d692f39f9f | |||
| 99d23a434c | |||
| 3cf7b8ddbe | |||
| 1d80bcb97c | |||
| 112f6f43fb | |||
| 8788d659c8 | |||
| be1982568a | |||
| ddd91793bc | |||
| 3eb797ddf7 | |||
| a67720552e | |||
| df69eb56d1 | |||
| 2036beec06 | |||
| 0fb8daa7bb | |||
| 10d17de5bc | |||
| 04f7d4a100 | |||
| eab2d9e3a9 | |||
| 3657c2a650 | |||
| f7f4930581 | |||
| 226ad23a1a | |||
| 535253e0d2 | |||
| 391e09331f | |||
| 6c127e2fdd | |||
| 57e0ffcb27 | |||
| d20e4d2fe9 | |||
| b77011a918 | |||
| c06d851ead | |||
| b95695b110 | |||
| 58aaddeca5 | |||
| 9c8e026003 | |||
| 88aa80e847 | |||
| 1addd35b78 | |||
| 6f661deb75 | |||
| bcc8de0c9d | |||
| 5c73f2c37f | |||
| f26dc79480 | |||
| 33a8e2226c | |||
| e7fbfa61e5 | |||
| 008e9f2101 | |||
| 9483507093 | |||
| 28df049b99 | |||
| 99ae04d9ce | |||
| 698146c366 | |||
| 94202f2664 | |||
| 09bb0720d4 | |||
| 9df998e710 | |||
| 0e50ba839c | |||
| f9b6b88d5c | |||
| 0d98f91ae6 | |||
| 0395eaacbb | |||
| 9c3bdf2647 | |||
| c795fb1937 | |||
| cb4e76060d | |||
| 41764b9c09 | |||
| 191d07d588 | |||
| 10a01423ec | |||
| 40e52e212a | |||
| 0d19a1333f | |||
| a22171507a | |||
| e33fcfddc1 | |||
| f428e6786b | |||
| 99e8849b6c | |||
| 3d36625af6 | |||
| bb4cf2830a | |||
| cdfce26ea9 | |||
| 553870d30a | |||
| 65a2b3b689 | |||
| 2020760f88 | |||
| 1a90998edc | |||
| fb3e60862f | |||
| 1185b5ee35 | |||
| c1dbc27fd6 | |||
| 8aae332622 | |||
| 94114b99f7 |
+2
-4
@@ -16,11 +16,9 @@
|
||||
*.ts @nextcloud/server-frontend
|
||||
|
||||
# dependency management
|
||||
package.json @nextcloud/server-dependabot
|
||||
package.json @nextcloud/server-dependabot @nextcloud/server-frontend
|
||||
package-lock.json @nextcloud/server-dependabot
|
||||
|
||||
# Compiled assets only - no owner set to not spam on automated dependency updates
|
||||
/dist
|
||||
/dist @nextcloud/server-dependabot
|
||||
|
||||
# App maintainers
|
||||
/apps/admin_audit/appinfo/info.xml @luka-nextcloud @blizzz
|
||||
|
||||
+1
-14
@@ -16,8 +16,6 @@ updates:
|
||||
labels:
|
||||
- "3. to review"
|
||||
- "feature: dependencies"
|
||||
reviewers:
|
||||
- "nextcloud/server-dependabot"
|
||||
|
||||
# Main composer (linting, testing, openapi)
|
||||
- package-ecosystem: composer
|
||||
@@ -28,6 +26,7 @@ updates:
|
||||
- "/vendor-bin/openapi-extractor"
|
||||
- "/vendor-bin/phpunit"
|
||||
- "/vendor-bin/psalm"
|
||||
- "/vendor-bin/rector"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
@@ -36,8 +35,6 @@ updates:
|
||||
labels:
|
||||
- "3. to review"
|
||||
- "feature: dependencies"
|
||||
reviewers:
|
||||
- "nextcloud/server-dependabot"
|
||||
|
||||
# Main master npm frontend dependencies
|
||||
- package-ecosystem: npm
|
||||
@@ -51,8 +48,6 @@ updates:
|
||||
labels:
|
||||
- "3. to review"
|
||||
- "feature: dependencies"
|
||||
reviewers:
|
||||
- "nextcloud/server-dependabot"
|
||||
# Disable automatic rebasing because without a build CI will likely fail anyway
|
||||
rebase-strategy: "disabled"
|
||||
|
||||
@@ -75,8 +70,6 @@ updates:
|
||||
labels:
|
||||
- "3. to review"
|
||||
- "feature: dependencies"
|
||||
reviewers:
|
||||
- "nextcloud/server-dependabot"
|
||||
ignore:
|
||||
# only patch updates on stable branches
|
||||
- dependency-name: "*"
|
||||
@@ -96,8 +89,6 @@ updates:
|
||||
labels:
|
||||
- "3. to review"
|
||||
- "feature: dependencies"
|
||||
reviewers:
|
||||
- "nextcloud/server-dependabot"
|
||||
# Disable automatic rebasing because without a build CI will likely fail anyway
|
||||
rebase-strategy: "disabled"
|
||||
ignore:
|
||||
@@ -125,8 +116,6 @@ updates:
|
||||
labels:
|
||||
- "3. to review"
|
||||
- "feature: dependencies"
|
||||
reviewers:
|
||||
- "nextcloud/server-dependabot"
|
||||
ignore:
|
||||
# only patch updates on stable branches
|
||||
- dependency-name: "*"
|
||||
@@ -145,8 +134,6 @@ updates:
|
||||
labels:
|
||||
- "3. to review"
|
||||
- "feature: dependencies"
|
||||
reviewers:
|
||||
- "nextcloud/server-dependabot"
|
||||
# Disable automatic rebasing because without a build CI will likely fail anyway
|
||||
rebase-strategy: "disabled"
|
||||
ignore:
|
||||
|
||||
@@ -94,11 +94,56 @@ jobs:
|
||||
matrix:
|
||||
# Run multiple copies of the current job in parallel
|
||||
# Please increase the number or runners as your tests suite grows (0 based index for e2e tests)
|
||||
containers: ["component", '0', '1', '2', '3', '4', '5', '6', '7']
|
||||
containers: ['component', 'setup', '0', '1', '2', '3', '4', '5', '6', '7']
|
||||
# Hack as strategy.job-total includes the component and GitHub does not allow math expressions
|
||||
# Always align this number with the total of e2e runners (max. index + 1)
|
||||
total-containers: [8]
|
||||
|
||||
services:
|
||||
mysql:
|
||||
# Only start mysql if we are running the setup tests
|
||||
image: ${{matrix.containers == 'setup' && 'ghcr.io/nextcloud/continuous-integration-mysql-8.4:latest' || ''}}
|
||||
ports:
|
||||
- '3306/tcp'
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: rootpassword
|
||||
MYSQL_USER: oc_autotest
|
||||
MYSQL_PASSWORD: nextcloud
|
||||
MYSQL_DATABASE: oc_autotest
|
||||
options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 10
|
||||
|
||||
mariadb:
|
||||
# Only start mariadb if we are running the setup tests
|
||||
image: ${{matrix.containers == 'setup' && 'mariadb:11.4' || ''}}
|
||||
ports:
|
||||
- '3306/tcp'
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: rootpassword
|
||||
MYSQL_USER: oc_autotest
|
||||
MYSQL_PASSWORD: nextcloud
|
||||
MYSQL_DATABASE: oc_autotest
|
||||
options: --health-cmd="mariadb-admin ping" --health-interval 5s --health-timeout 2s --health-retries 5
|
||||
|
||||
postgres:
|
||||
# Only start postgres if we are running the setup tests
|
||||
image: ${{matrix.containers == 'setup' && 'ghcr.io/nextcloud/continuous-integration-postgres-17:latest' || ''}}
|
||||
ports:
|
||||
- '5432/tcp'
|
||||
env:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: rootpassword
|
||||
POSTGRES_DB: nextcloud
|
||||
options: --mount type=tmpfs,destination=/var/lib/postgresql/data --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
|
||||
|
||||
oracle:
|
||||
# Only start oracle if we are running the setup tests
|
||||
image: ${{matrix.containers == 'setup' && 'ghcr.io/gvenzl/oracle-free:23' || ''}}
|
||||
ports:
|
||||
- '1521'
|
||||
env:
|
||||
ORACLE_PASSWORD: oracle
|
||||
options: --health-cmd healthcheck.sh --health-interval 20s --health-timeout 10s --health-retries 10
|
||||
|
||||
name: runner ${{ matrix.containers }}
|
||||
|
||||
steps:
|
||||
@@ -141,6 +186,7 @@ jobs:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
SPLIT: ${{ matrix.total-containers }}
|
||||
SPLIT_INDEX: ${{ matrix.containers == 'component' && 0 || matrix.containers }}
|
||||
SETUP_TESTING: ${{ matrix.containers == 'setup' && 'true' || '' }}
|
||||
|
||||
- name: Upload snapshots and videos
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
|
||||
@@ -96,15 +96,22 @@ jobs:
|
||||
- name: PHPUnit
|
||||
run: composer run test:files_external -- \
|
||||
apps/files_external/tests/Storage/FtpTest.php \
|
||||
${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
--log-junit junit.xml \
|
||||
${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-ftp
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-files-external-ftp
|
||||
|
||||
- name: ftpd logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -94,15 +94,22 @@ jobs:
|
||||
run: composer run test:files_external -- \
|
||||
apps/files_external/tests/Storage/Amazons3Test.php \
|
||||
apps/files_external/tests/Storage/VersionedAmazonS3Test.php \
|
||||
${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
--log-junit junit.xml \
|
||||
${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-s3
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-files-external-s3
|
||||
|
||||
- name: S3 logs
|
||||
if: always()
|
||||
run: |
|
||||
@@ -161,15 +168,22 @@ jobs:
|
||||
run: composer run test:files_external -- \
|
||||
apps/files_external/tests/Storage/Amazons3Test.php \
|
||||
apps/files_external/tests/Storage/VersionedAmazonS3Test.php \
|
||||
${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
--log-junit junit.xml \
|
||||
${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-s3
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-files-external-s3
|
||||
|
||||
- name: S3 logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -85,15 +85,22 @@ jobs:
|
||||
run: composer run test:files_external -- \
|
||||
apps/files_external/tests/Storage/SftpTest.php \
|
||||
apps/files_external/tests/Storage/SFTP_KeyTest.php \
|
||||
${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
--log-junit junit.xml \
|
||||
${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-sftp
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-files-external-sftp
|
||||
|
||||
- name: sftpd logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -83,6 +83,18 @@ jobs:
|
||||
run: |
|
||||
apps/files_external/tests/sso-setup/test-sso-smb.sh ${{ env.DC_IP }}
|
||||
|
||||
- name: Show logs DC
|
||||
if: always()
|
||||
run: |
|
||||
docker logs dc
|
||||
echo "------------"
|
||||
docker exec dc cat /var/log/samba/log.samba
|
||||
|
||||
- name: Show logs Apache
|
||||
if: always()
|
||||
run: |
|
||||
docker logs apache
|
||||
|
||||
- name: Show logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -90,15 +90,22 @@ jobs:
|
||||
- name: PHPUnit
|
||||
run: composer run test:files_external -- --verbose \
|
||||
apps/files_external/tests/Storage/SmbTest.php \
|
||||
${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
--log-junit junit.xml \
|
||||
${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v4.1.1
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-smb
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-files-external-smb
|
||||
|
||||
files-external-smb-summary:
|
||||
runs-on: ubuntu-latest-low
|
||||
needs: [changes, files-external-smb]
|
||||
|
||||
@@ -87,15 +87,22 @@ jobs:
|
||||
- name: PHPUnit
|
||||
run: composer run test:files_external -- --verbose \
|
||||
apps/files_external/tests/Storage/WebdavTest.php \
|
||||
${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
--log-junit junit.xml \
|
||||
${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v4.1.1
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-webdav
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-files-external-webdav
|
||||
|
||||
files-external-webdav-summary:
|
||||
runs-on: ubuntu-latest-low
|
||||
needs: [changes, files-external-webdav-apache]
|
||||
|
||||
@@ -75,15 +75,22 @@ jobs:
|
||||
|
||||
- name: PHPUnit
|
||||
run: composer run test:files_external -- \
|
||||
${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
--log-junit junit.xml \
|
||||
${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-files-external-generic
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-files-external-generic
|
||||
|
||||
files-external-summary:
|
||||
runs-on: ubuntu-latest-low
|
||||
needs: [changes, files-external-generic ]
|
||||
|
||||
@@ -99,10 +99,14 @@ jobs:
|
||||
run: npm run test:coverage --if-present
|
||||
|
||||
- name: Collect coverage
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v4.3.1
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
|
||||
jsunit:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [versions, changes]
|
||||
|
||||
@@ -101,15 +101,21 @@ jobs:
|
||||
OBJECT_STORE: azure
|
||||
OBJECT_STORE_KEY: nextcloud
|
||||
OBJECT_STORE_SECRET: bmV4dGNsb3Vk
|
||||
run: composer run test -- --group PRIMARY-azure ${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
run: composer run test -- --group PRIMARY-azure --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-azure
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-azure
|
||||
|
||||
- name: Azurite logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -107,15 +107,21 @@ jobs:
|
||||
OBJECT_STORE: s3
|
||||
OBJECT_STORE_KEY: nextcloud
|
||||
OBJECT_STORE_SECRET: bWluaW8tc2VjcmV0LWtleS1uZXh0Y2xvdWQ=
|
||||
run: composer run test -- --group PRIMARY-s3 ${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
run: composer run test -- --group PRIMARY-s3 --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-s3
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-s3
|
||||
|
||||
- name: S3 logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -97,15 +97,21 @@ jobs:
|
||||
env:
|
||||
OBJECT_STORE: swift
|
||||
OBJECT_STORE_SECRET: veryfast
|
||||
run: composer run test -- --group PRIMARY-swift ${{ matrix.coverage && ' --coverage-clover ./clover.xml' || '' }}
|
||||
run: composer run test -- --group PRIMARY-swift --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-swift
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-swift
|
||||
|
||||
- name: Swift logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -120,15 +120,21 @@ jobs:
|
||||
php -f tests/enable_all.php | grep -i -C9999 error && echo "Error during app setup" && exit 1 || exit 0
|
||||
|
||||
- name: PHPUnit
|
||||
run: composer run test:db ${{ matrix.coverage && ' -- --coverage-clover ./clover.db.xml' || '' }}
|
||||
run: composer run test:db -- --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.db.xml' || '' }}
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-mariadb
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-mariadb
|
||||
|
||||
summary:
|
||||
permissions:
|
||||
contents: none
|
||||
|
||||
@@ -97,15 +97,21 @@ jobs:
|
||||
php -f tests/enable_all.php | grep -i -C9999 error && echo "Error during app setup" && exit 1 || exit 0
|
||||
|
||||
- name: PHPUnit memcached tests
|
||||
run: composer run test -- --group Memcache,Memcached ${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
run: composer run test -- --group Memcache,Memcached --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.xml' || '' }}
|
||||
|
||||
- name: Upload code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.xml
|
||||
flags: phpunit-memcached
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-memcached
|
||||
|
||||
- name: Print logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -155,15 +155,21 @@ jobs:
|
||||
php -f tests/enable_all.php | grep -i -C9999 error && echo "Error during app setup" && exit 1 || exit 0
|
||||
|
||||
- name: PHPUnit
|
||||
run: composer run test:db ${{ matrix.coverage && ' -- --coverage-clover ./clover.db.xml' || '' }}
|
||||
run: composer run test:db -- --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.db.xml' || '' }}
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-mysql
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-mysql
|
||||
|
||||
- name: Print logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -120,15 +120,21 @@ jobs:
|
||||
php -f tests/enable_all.php | grep -i -C9999 error && echo "Error during app setup" && exit 1 || exit 0
|
||||
|
||||
- name: PHPUnit
|
||||
run: composer run test:db ${{ matrix.coverage && ' -- --coverage-clover ./clover.db.xml' || '' }}
|
||||
run: composer run test:db -- --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.db.xml' || '' }}
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-mysql
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-mysql
|
||||
|
||||
- name: Print logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -103,15 +103,21 @@ jobs:
|
||||
php -f tests/enable_all.php | grep -i -C9999 error && echo "Error during app setup" && exit 1 || exit 0
|
||||
|
||||
- name: PHPUnit nodb testsuite
|
||||
run: composer run test -- --exclude-group DB,SLOWDB ${{ matrix.coverage && ' --coverage-clover ./clover.nodb.xml' || '' }}
|
||||
run: composer run test -- --exclude-group DB,SLOWDB --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.nodb.xml' || '' }}
|
||||
|
||||
- name: Upload nodb code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.nodb.xml
|
||||
flags: phpunit-nodb
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-nodb
|
||||
|
||||
- name: Print logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -125,15 +125,21 @@ jobs:
|
||||
php -f tests/enable_all.php | grep -i -C9999 error && echo "Error during app setup" && exit 1 || exit 0
|
||||
|
||||
- name: PHPUnit
|
||||
run: composer run test:db ${{ matrix.coverage && ' -- --coverage-clover ./clover.db.xml' || '' }}
|
||||
run: composer run test:db -- --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.db.xml' || '' }}
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-oci
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-oci
|
||||
|
||||
- name: Run repair steps
|
||||
run: |
|
||||
./occ maintenance:repair --include-expensive
|
||||
|
||||
@@ -115,15 +115,21 @@ jobs:
|
||||
php -f tests/enable_all.php | grep -i -C9999 error && echo "Error during app setup" && exit 1 || exit 0
|
||||
|
||||
- name: PHPUnit database tests
|
||||
run: composer run test:db ${{ matrix.coverage && ' -- --coverage-clover ./clover.db.xml' || '' }}
|
||||
run: composer run test:db -- --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.db.xml' || '' }}
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-postgres
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-postgres
|
||||
|
||||
- name: Run repair steps
|
||||
run: |
|
||||
./occ maintenance:repair --include-expensive
|
||||
|
||||
@@ -101,15 +101,21 @@ jobs:
|
||||
run: ./occ app:list && echo "======= System config =======" && ./occ config:list system
|
||||
|
||||
- name: PHPUnit database tests
|
||||
run: composer run test:db ${{ matrix.coverage && ' -- --coverage-clover ./clover.db.xml' || '' }}
|
||||
run: composer run test:db -- --log-junit junit.xml ${{ matrix.coverage && '--coverage-clover ./clover.db.xml' || '' }}
|
||||
|
||||
- name: Upload db code coverage
|
||||
if: ${{ !cancelled() && matrix.coverage }}
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./clover.db.xml
|
||||
flags: phpunit-sqlite
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
|
||||
with:
|
||||
flags: phpunit-sqlite
|
||||
|
||||
- name: Print logs
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace OCA\AdminAudit\Actions;
|
||||
use OC\Files\Node\NonExistingFile;
|
||||
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
|
||||
use OCP\Files\Events\Node\BeforeNodeReadEvent;
|
||||
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
|
||||
use OCP\Files\Events\Node\NodeCopiedEvent;
|
||||
use OCP\Files\Events\Node\NodeCreatedEvent;
|
||||
use OCP\Files\Events\Node\NodeRenamedEvent;
|
||||
@@ -26,9 +25,6 @@ use Psr\Log\LoggerInterface;
|
||||
* @package OCA\AdminAudit\Actions
|
||||
*/
|
||||
class Files extends Action {
|
||||
|
||||
private array $renamedNodes = [];
|
||||
|
||||
/**
|
||||
* Logs file read actions
|
||||
*/
|
||||
@@ -52,31 +48,16 @@ class Files extends Action {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs rename actions of files
|
||||
*/
|
||||
public function beforeRename(BeforeNodeRenamedEvent $event): void {
|
||||
try {
|
||||
$source = $event->getSource();
|
||||
$this->renamedNodes[$source->getId()] = $source;
|
||||
} catch (InvalidPathException|NotFoundException $e) {
|
||||
Server::get(LoggerInterface::class)->error(
|
||||
'Exception thrown in file rename: ' . $e->getMessage(), ['app' => 'admin_audit', 'exception' => $e]
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs rename actions of files
|
||||
*/
|
||||
public function afterRename(NodeRenamedEvent $event): void {
|
||||
try {
|
||||
$target = $event->getTarget();
|
||||
$originalSource = $this->renamedNodes[$target->getId()];
|
||||
$source = $event->getSource();
|
||||
$params = [
|
||||
'newid' => $target->getId(),
|
||||
'oldpath' => $originalSource->getPath(),
|
||||
'oldpath' => $source->getPath(),
|
||||
'newpath' => $target->getPath(),
|
||||
];
|
||||
} catch (InvalidPathException|NotFoundException $e) {
|
||||
|
||||
@@ -42,7 +42,6 @@ use OCP\Console\ConsoleEvent;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
|
||||
use OCP\Files\Events\Node\BeforeNodeReadEvent;
|
||||
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
|
||||
use OCP\Files\Events\Node\NodeCopiedEvent;
|
||||
use OCP\Files\Events\Node\NodeCreatedEvent;
|
||||
use OCP\Files\Events\Node\NodeRenamedEvent;
|
||||
@@ -170,13 +169,6 @@ class Application extends App implements IBootstrap {
|
||||
private function fileHooks(IAuditLogger $logger, IEventDispatcher $eventDispatcher): void {
|
||||
$fileActions = new Files($logger);
|
||||
|
||||
$eventDispatcher->addListener(
|
||||
BeforeNodeRenamedEvent::class,
|
||||
function (BeforeNodeRenamedEvent $event) use ($fileActions): void {
|
||||
$fileActions->beforeRename($event);
|
||||
}
|
||||
);
|
||||
|
||||
$eventDispatcher->addListener(
|
||||
NodeRenamedEvent::class,
|
||||
function (NodeRenamedEvent $event) use ($fileActions): void {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
OC.L10N.register(
|
||||
"cloud_federation_api",
|
||||
{
|
||||
"Cloud Federation API" : "Asl faylni o'chirishda kutilmagan xatolik yuz berdi.",
|
||||
"Enable clouds to communicate with each other and exchange data" : "Bulutlar bir-biri bilan aloqa qilish va ma'lumot almashish imkonini beradi",
|
||||
"The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API turli xil Nextcloud misollariga bir-biri bilan muloqot qilish va ma'lumotlarni almashish imkonini beradi."
|
||||
},
|
||||
"nplurals=1; plural=0;");
|
||||
@@ -0,0 +1,6 @@
|
||||
{ "translations": {
|
||||
"Cloud Federation API" : "Asl faylni o'chirishda kutilmagan xatolik yuz berdi.",
|
||||
"Enable clouds to communicate with each other and exchange data" : "Bulutlar bir-biri bilan aloqa qilish va ma'lumot almashish imkonini beradi",
|
||||
"The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API turli xil Nextcloud misollariga bir-biri bilan muloqot qilish va ma'lumotlarni almashish imkonini beradi."
|
||||
},"pluralForm" :"nplurals=1; plural=0;"
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -20,30 +23,17 @@ use OCP\Files\Node;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Share\IShareHelper;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class ListenerTest extends TestCase {
|
||||
|
||||
/** @var Listener */
|
||||
protected $listener;
|
||||
|
||||
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $activityManager;
|
||||
|
||||
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $session;
|
||||
|
||||
/** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $appManager;
|
||||
|
||||
/** @var IMountProviderCollection|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $mountProviderCollection;
|
||||
|
||||
/** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $rootFolder;
|
||||
|
||||
/** @var IShareHelper|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $shareHelper;
|
||||
protected IManager&MockObject $activityManager;
|
||||
protected IUserSession&MockObject $session;
|
||||
protected IAppManager&MockObject $appManager;
|
||||
protected IMountProviderCollection&MockObject $mountProviderCollection;
|
||||
protected IRootFolder&MockObject $rootFolder;
|
||||
protected IShareHelper&MockObject $shareHelper;
|
||||
protected Listener $listener;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
@@ -76,7 +66,7 @@ class ListenerTest extends TestCase {
|
||||
->method('getObjectType')
|
||||
->willReturn('files');
|
||||
|
||||
/** @var CommentsEvent|\PHPUnit\Framework\MockObject\MockObject $event */
|
||||
/** @var CommentsEvent|MockObject $event */
|
||||
$event = $this->createMock(CommentsEvent::class);
|
||||
$event->expects($this->any())
|
||||
->method('getComment')
|
||||
@@ -85,13 +75,13 @@ class ListenerTest extends TestCase {
|
||||
->method('getEvent')
|
||||
->willReturn(CommentsEvent::EVENT_ADD);
|
||||
|
||||
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject $ownerUser */
|
||||
/** @var IUser|MockObject $ownerUser */
|
||||
$ownerUser = $this->createMock(IUser::class);
|
||||
$ownerUser->expects($this->any())
|
||||
->method('getUID')
|
||||
->willReturn('937393');
|
||||
|
||||
/** @var \PHPUnit\Framework\MockObject\MockObject $mount */
|
||||
/** @var MockObject $mount */
|
||||
$mount = $this->createMock(ICachedMountFileInfo::class);
|
||||
$mount->expects($this->any())
|
||||
->method('getUser')
|
||||
@@ -133,7 +123,7 @@ class ListenerTest extends TestCase {
|
||||
->method('getUser')
|
||||
->willReturn($ownerUser);
|
||||
|
||||
/** @var \PHPUnit\Framework\MockObject\MockObject $activity */
|
||||
/** @var MockObject $activity */
|
||||
$activity = $this->createMock(IEvent::class);
|
||||
$activity->expects($this->exactly(count($al['users'])))
|
||||
->method('setAffectedUser');
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
@@ -7,7 +9,12 @@
|
||||
*/
|
||||
namespace OCA\Comments\Tests\Unit\AppInfo;
|
||||
|
||||
use OCA\Comments\Activity\Filter;
|
||||
use OCA\Comments\Activity\Listener;
|
||||
use OCA\Comments\Activity\Provider;
|
||||
use OCA\Comments\Activity\Setting;
|
||||
use OCA\Comments\AppInfo\Application;
|
||||
use OCA\Comments\Controller\NotificationsController;
|
||||
use OCA\Comments\Notification\Notifier;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IUserSession;
|
||||
@@ -38,12 +45,12 @@ class ApplicationTest extends TestCase {
|
||||
$c = $app->getContainer();
|
||||
|
||||
$services = [
|
||||
'OCA\Comments\Controller\NotificationsController',
|
||||
'OCA\Comments\Activity\Filter',
|
||||
'OCA\Comments\Activity\Listener',
|
||||
'OCA\Comments\Activity\Provider',
|
||||
'OCA\Comments\Activity\Setting',
|
||||
'OCA\Comments\Notification\Listener',
|
||||
NotificationsController::class,
|
||||
Filter::class,
|
||||
Listener::class,
|
||||
Provider::class,
|
||||
Setting::class,
|
||||
\OCA\Comments\Notification\Listener::class,
|
||||
Notifier::class,
|
||||
];
|
||||
|
||||
|
||||
@@ -8,13 +8,12 @@ namespace OCA\Comments\Tests\Unit\Collaboration;
|
||||
use OCA\Comments\Collaboration\CommentersSorter;
|
||||
use OCP\Comments\IComment;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class CommentersSorterTest extends TestCase {
|
||||
/** @var ICommentsManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $commentsManager;
|
||||
/** @var CommentersSorter */
|
||||
protected $sorter;
|
||||
protected ICommentsManager&MockObject $commentsManager;
|
||||
protected CommentersSorter $sorter;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
@@ -55,7 +54,7 @@ class CommentersSorterTest extends TestCase {
|
||||
$this->assertEquals($data['expected'], $workArray);
|
||||
}
|
||||
|
||||
public function sortDataProvider() {
|
||||
public static function sortDataProvider(): array {
|
||||
return [[
|
||||
[
|
||||
#1 – sort properly and otherwise keep existing order
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
@@ -22,26 +24,16 @@ use OCP\IUser;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Notification\IManager;
|
||||
use OCP\Notification\INotification;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class NotificationsTest extends TestCase {
|
||||
/** @var NotificationsController */
|
||||
protected $notificationsController;
|
||||
|
||||
/** @var ICommentsManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $commentsManager;
|
||||
|
||||
/** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $rootFolder;
|
||||
|
||||
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $session;
|
||||
|
||||
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $notificationManager;
|
||||
|
||||
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $urlGenerator;
|
||||
protected ICommentsManager&MockObject $commentsManager;
|
||||
protected IRootFolder&MockObject $rootFolder;
|
||||
protected IUserSession&MockObject $session;
|
||||
protected IManager&MockObject $notificationManager;
|
||||
protected IURLGenerator&MockObject $urlGenerator;
|
||||
protected NotificationsController $notificationsController;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
@@ -81,10 +73,6 @@ class NotificationsTest extends TestCase {
|
||||
|
||||
$this->urlGenerator->expects($this->exactly(2))
|
||||
->method('linkToRoute')
|
||||
->withConsecutive(
|
||||
['comments.Notifications.view', ['id' => '42']],
|
||||
['core.login.showLoginForm', ['redirect_url' => 'link-to-comment']]
|
||||
)
|
||||
->willReturnMap([
|
||||
['comments.Notifications.view', ['id' => '42'], 'link-to-comment'],
|
||||
['core.login.showLoginForm', ['redirect_url' => 'link-to-comment'], 'link-to-login'],
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -10,43 +13,32 @@ use OCA\Comments\Listener\CommentsEventListener;
|
||||
use OCA\Comments\Notification\Listener as NotificationListener;
|
||||
use OCP\Comments\CommentsEvent;
|
||||
use OCP\Comments\IComment;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class EventHandlerTest extends TestCase {
|
||||
/** @var CommentsEventListener */
|
||||
protected $eventHandler;
|
||||
|
||||
/** @var ActivityListener|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $activityListener;
|
||||
|
||||
/** @var NotificationListener|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $notificationListener;
|
||||
protected ActivityListener&MockObject $activityListener;
|
||||
protected NotificationListener&MockObject $notificationListener;
|
||||
protected CommentsEventListener $eventHandler;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->activityListener = $this->getMockBuilder(ActivityListener::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$this->notificationListener = $this->getMockBuilder(NotificationListener::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$this->activityListener = $this->createMock(ActivityListener::class);
|
||||
$this->notificationListener = $this->createMock(NotificationListener::class);
|
||||
|
||||
$this->eventHandler = new CommentsEventListener($this->activityListener, $this->notificationListener);
|
||||
}
|
||||
|
||||
public function testNotFiles(): void {
|
||||
/** @var IComment|\PHPUnit\Framework\MockObject\MockObject $comment */
|
||||
$comment = $this->getMockBuilder(IComment::class)->getMock();
|
||||
/** @var IComment|MockObject $comment */
|
||||
$comment = $this->createMock(IComment::class);
|
||||
$comment->expects($this->once())
|
||||
->method('getObjectType')
|
||||
->willReturn('smiles');
|
||||
|
||||
/** @var CommentsEvent|\PHPUnit\Framework\MockObject\MockObject $event */
|
||||
$event = $this->getMockBuilder(CommentsEvent::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
/** @var CommentsEvent|MockObject $event */
|
||||
$event = $this->createMock(CommentsEvent::class);
|
||||
$event->expects($this->once())
|
||||
->method('getComment')
|
||||
->willReturn($comment);
|
||||
@@ -56,7 +48,7 @@ class EventHandlerTest extends TestCase {
|
||||
$this->eventHandler->handle($event);
|
||||
}
|
||||
|
||||
public function handledProvider() {
|
||||
public static function handledProvider(): array {
|
||||
return [
|
||||
[CommentsEvent::EVENT_DELETE],
|
||||
[CommentsEvent::EVENT_UPDATE],
|
||||
@@ -67,19 +59,16 @@ class EventHandlerTest extends TestCase {
|
||||
|
||||
/**
|
||||
* @dataProvider handledProvider
|
||||
* @param string $eventType
|
||||
*/
|
||||
public function testHandled($eventType): void {
|
||||
/** @var IComment|\PHPUnit\Framework\MockObject\MockObject $comment */
|
||||
$comment = $this->getMockBuilder(IComment::class)->getMock();
|
||||
public function testHandled(string $eventType): void {
|
||||
/** @var IComment|MockObject $comment */
|
||||
$comment = $this->createMock(IComment::class);
|
||||
$comment->expects($this->once())
|
||||
->method('getObjectType')
|
||||
->willReturn('files');
|
||||
|
||||
/** @var CommentsEvent|\PHPUnit\Framework\MockObject\MockObject $event */
|
||||
$event = $this->getMockBuilder(CommentsEvent::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
/** @var CommentsEvent|MockObject $event */
|
||||
$event = $this->createMock(CommentsEvent::class);
|
||||
$event->expects($this->atLeastOnce())
|
||||
->method('getComment')
|
||||
->willReturn($comment);
|
||||
|
||||
@@ -14,25 +14,19 @@ use OCP\IURLGenerator;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Notification\IManager;
|
||||
use OCP\Notification\INotification;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class ListenerTest extends TestCase {
|
||||
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $notificationManager;
|
||||
|
||||
/** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $userManager;
|
||||
|
||||
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
|
||||
protected $urlGenerator;
|
||||
|
||||
/** @var Listener */
|
||||
protected $listener;
|
||||
protected IManager&MockObject $notificationManager;
|
||||
protected IUserManager&MockObject $userManager;
|
||||
protected IURLGenerator&MockObject $urlGenerator;
|
||||
protected Listener $listener;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->notificationManager = $this->createMock(\OCP\Notification\IManager::class);
|
||||
$this->notificationManager = $this->createMock(IManager::class);
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
|
||||
$this->listener = new Listener(
|
||||
@@ -41,7 +35,7 @@ class ListenerTest extends TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
public function eventProvider() {
|
||||
public static function eventProvider(): array {
|
||||
return [
|
||||
[CommentsEvent::EVENT_ADD, 'notify'],
|
||||
[CommentsEvent::EVENT_UPDATE, 'notify'],
|
||||
@@ -56,8 +50,8 @@ class ListenerTest extends TestCase {
|
||||
* @param string $notificationMethod
|
||||
*/
|
||||
public function testEvaluate($eventType, $notificationMethod): void {
|
||||
/** @var IComment|\PHPUnit\Framework\MockObject\MockObject $comment */
|
||||
$comment = $this->getMockBuilder(IComment::class)->getMock();
|
||||
/** @var IComment|MockObject $comment */
|
||||
$comment = $this->createMock(IComment::class);
|
||||
$comment->expects($this->any())
|
||||
->method('getObjectType')
|
||||
->willReturn('files');
|
||||
@@ -78,10 +72,8 @@ class ListenerTest extends TestCase {
|
||||
->method('getId')
|
||||
->willReturn('1234');
|
||||
|
||||
/** @var CommentsEvent|\PHPUnit\Framework\MockObject\MockObject $event */
|
||||
$event = $this->getMockBuilder(CommentsEvent::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
/** @var CommentsEvent|MockObject $event */
|
||||
$event = $this->createMock(CommentsEvent::class);
|
||||
$event->expects($this->once())
|
||||
->method('getComment')
|
||||
->willReturn($comment);
|
||||
@@ -89,8 +81,8 @@ class ListenerTest extends TestCase {
|
||||
->method(('getEvent'))
|
||||
->willReturn($eventType);
|
||||
|
||||
/** @var INotification|\PHPUnit\Framework\MockObject\MockObject $notification */
|
||||
$notification = $this->getMockBuilder(INotification::class)->getMock();
|
||||
/** @var INotification|MockObject $notification */
|
||||
$notification = $this->createMock(INotification::class);
|
||||
$notification->expects($this->any())
|
||||
->method($this->anything())
|
||||
->willReturn($notification);
|
||||
@@ -106,26 +98,24 @@ class ListenerTest extends TestCase {
|
||||
|
||||
$this->userManager->expects($this->exactly(6))
|
||||
->method('userExists')
|
||||
->withConsecutive(
|
||||
['foobar'],
|
||||
['barfoo'],
|
||||
['foo@bar.com'],
|
||||
['bar@foo.org@foobar.io'],
|
||||
['23452-4333-54353-2342'],
|
||||
['yolo']
|
||||
)
|
||||
->willReturn(true);
|
||||
->willReturnMap([
|
||||
['foobar', true],
|
||||
['barfoo', true],
|
||||
['foo@bar.com', true],
|
||||
['bar@foo.org@foobar.io', true],
|
||||
['23452-4333-54353-2342', true],
|
||||
['yolo', true]
|
||||
]);
|
||||
|
||||
$this->listener->evaluate($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider eventProvider
|
||||
* @param string $eventType
|
||||
*/
|
||||
public function testEvaluateNoMentions($eventType): void {
|
||||
/** @var IComment|\PHPUnit\Framework\MockObject\MockObject $comment */
|
||||
$comment = $this->getMockBuilder(IComment::class)->getMock();
|
||||
public function testEvaluateNoMentions(string $eventType): void {
|
||||
/** @var IComment|MockObject $comment */
|
||||
$comment = $this->createMock(IComment::class);
|
||||
$comment->expects($this->any())
|
||||
->method('getObjectType')
|
||||
->willReturn('files');
|
||||
@@ -136,10 +126,8 @@ class ListenerTest extends TestCase {
|
||||
->method('getMentions')
|
||||
->willReturn([]);
|
||||
|
||||
/** @var CommentsEvent|\PHPUnit\Framework\MockObject\MockObject $event */
|
||||
$event = $this->getMockBuilder(CommentsEvent::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
/** @var CommentsEvent|MockObject $event */
|
||||
$event = $this->createMock(CommentsEvent::class);
|
||||
$event->expects($this->once())
|
||||
->method('getComment')
|
||||
->willReturn($comment);
|
||||
@@ -161,8 +149,8 @@ class ListenerTest extends TestCase {
|
||||
}
|
||||
|
||||
public function testEvaluateUserDoesNotExist(): void {
|
||||
/** @var IComment|\PHPUnit\Framework\MockObject\MockObject $comment */
|
||||
$comment = $this->getMockBuilder(IComment::class)->getMock();
|
||||
/** @var IComment|MockObject $comment */
|
||||
$comment = $this->createMock(IComment::class);
|
||||
$comment->expects($this->any())
|
||||
->method('getObjectType')
|
||||
->willReturn('files');
|
||||
@@ -176,10 +164,8 @@ class ListenerTest extends TestCase {
|
||||
->method('getId')
|
||||
->willReturn('1234');
|
||||
|
||||
/** @var CommentsEvent|\PHPUnit\Framework\MockObject\MockObject $event */
|
||||
$event = $this->getMockBuilder(CommentsEvent::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
/** @var CommentsEvent|MockObject $event */
|
||||
$event = $this->createMock(CommentsEvent::class);
|
||||
$event->expects($this->once())
|
||||
->method('getComment')
|
||||
->willReturn($comment);
|
||||
@@ -187,8 +173,8 @@ class ListenerTest extends TestCase {
|
||||
->method(('getEvent'))
|
||||
->willReturn(CommentsEvent::EVENT_ADD);
|
||||
|
||||
/** @var INotification|\PHPUnit\Framework\MockObject\MockObject $notification */
|
||||
$notification = $this->getMockBuilder(INotification::class)->getMock();
|
||||
/** @var INotification|MockObject $notification */
|
||||
$notification = $this->createMock(INotification::class);
|
||||
$notification->expects($this->any())
|
||||
->method($this->anything())
|
||||
->willReturn($notification);
|
||||
@@ -203,9 +189,7 @@ class ListenerTest extends TestCase {
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('userExists')
|
||||
->withConsecutive(
|
||||
['foobar']
|
||||
)
|
||||
->with('foobar')
|
||||
->willReturn(false);
|
||||
|
||||
$this->listener->evaluate($event);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -23,26 +26,16 @@ use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class NotifierTest extends TestCase {
|
||||
/** @var Notifier */
|
||||
protected $notifier;
|
||||
/** @var IFactory|MockObject */
|
||||
protected $l10nFactory;
|
||||
/** @var IL10N|MockObject */
|
||||
protected $l;
|
||||
/** @var IRootFolder|MockObject */
|
||||
protected $folder;
|
||||
/** @var ICommentsManager|MockObject */
|
||||
protected $commentsManager;
|
||||
/** @var IURLGenerator|MockObject */
|
||||
protected $url;
|
||||
/** @var IUserManager|MockObject */
|
||||
protected $userManager;
|
||||
/** @var INotification|MockObject */
|
||||
protected $notification;
|
||||
/** @var IComment|MockObject */
|
||||
protected $comment;
|
||||
/** @var string */
|
||||
protected $lc = 'tlh_KX';
|
||||
protected IFactory&MockObject $l10nFactory;
|
||||
protected IL10N&MockObject $l;
|
||||
protected IRootFolder&MockObject $folder;
|
||||
protected ICommentsManager&MockObject $commentsManager;
|
||||
protected IURLGenerator&MockObject $url;
|
||||
protected IUserManager&MockObject $userManager;
|
||||
protected INotification&MockObject $notification;
|
||||
protected IComment&MockObject $comment;
|
||||
protected Notifier $notifier;
|
||||
protected string $lc = 'tlh_KX';
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
@@ -75,9 +68,8 @@ class NotifierTest extends TestCase {
|
||||
public function testPrepareSuccess(): void {
|
||||
$fileName = 'Gre\'thor.odp';
|
||||
$displayName = 'Huraga';
|
||||
$message = '@Huraga mentioned you in a comment on "Gre\'thor.odp"';
|
||||
|
||||
/** @var Node|MockObject $node */
|
||||
/** @var Node&MockObject $node */
|
||||
$node = $this->createMock(Node::class);
|
||||
$node
|
||||
->expects($this->atLeastOnce())
|
||||
@@ -192,7 +184,6 @@ class NotifierTest extends TestCase {
|
||||
|
||||
public function testPrepareSuccessDeletedUser(): void {
|
||||
$fileName = 'Gre\'thor.odp';
|
||||
$message = 'You were mentioned on "Gre\'thor.odp", in a comment by an account that has since been deleted';
|
||||
|
||||
/** @var Node|MockObject $node */
|
||||
$node = $this->createMock(Node::class);
|
||||
|
||||
@@ -21,12 +21,8 @@ use Test\TestCase;
|
||||
* @group DB
|
||||
*/
|
||||
class RecentContactMapperTest extends TestCase {
|
||||
|
||||
/** @var RecentContactMapper */
|
||||
private $recentContactMapper;
|
||||
|
||||
/** @var ITimeFactory */
|
||||
private $time;
|
||||
private RecentContactMapper $recentContactMapper;
|
||||
private ITimeFactory $time;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
@@ -13,6 +13,8 @@ OC.L10N.register(
|
||||
"Good evening, {name}" : "Bună seara, {name}",
|
||||
"Hello" : "Bună",
|
||||
"Hello, {name}" : "Bună, {name}",
|
||||
"Happy birthday 🥳🤩🎂🎉" : "La mulți ani 🥳🤩🎂🎉",
|
||||
"Happy birthday, {name} 🥳🤩🎂🎉" : "La mulți ani, {name} 🥳🤩🎂🎉",
|
||||
"Customize" : "Personalizează",
|
||||
"Edit widgets" : "Editează widget",
|
||||
"Get more widgets from the App Store" : "Obține mai multe widget-uri din App Store",
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"Good evening, {name}" : "Bună seara, {name}",
|
||||
"Hello" : "Bună",
|
||||
"Hello, {name}" : "Bună, {name}",
|
||||
"Happy birthday 🥳🤩🎂🎉" : "La mulți ani 🥳🤩🎂🎉",
|
||||
"Happy birthday, {name} 🥳🤩🎂🎉" : "La mulți ani, {name} 🥳🤩🎂🎉",
|
||||
"Customize" : "Personalizează",
|
||||
"Edit widgets" : "Editează widget",
|
||||
"Get more widgets from the App Store" : "Obține mai multe widget-uri din App Store",
|
||||
|
||||
@@ -40,7 +40,7 @@ class DashboardServiceTest extends TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetBirthdate() {
|
||||
public function testGetBirthdate(): void {
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->userManager->method('get')
|
||||
->willReturn($user);
|
||||
@@ -61,7 +61,7 @@ class DashboardServiceTest extends TestCase {
|
||||
$this->assertEquals('2024-12-10T00:00:00.000Z', $birthdate);
|
||||
}
|
||||
|
||||
public function testGetBirthdatePropertyDoesNotExist() {
|
||||
public function testGetBirthdatePropertyDoesNotExist(): void {
|
||||
$user = $this->createMock(IUser::class);
|
||||
$this->userManager->method('get')
|
||||
->willReturn($user);
|
||||
@@ -75,7 +75,7 @@ class DashboardServiceTest extends TestCase {
|
||||
$this->assertEquals('', $birthdate);
|
||||
}
|
||||
|
||||
public function testGetBirthdateUserNotFound() {
|
||||
public function testGetBirthdateUserNotFound(): void {
|
||||
$this->userManager->method('get')
|
||||
->willReturn(null);
|
||||
|
||||
@@ -84,7 +84,7 @@ class DashboardServiceTest extends TestCase {
|
||||
$this->assertEquals('', $birthdate);
|
||||
}
|
||||
|
||||
public function testGetBirthdateNoUserId() {
|
||||
public function testGetBirthdateNoUserId(): void {
|
||||
$service = new DashboardService(
|
||||
$this->config,
|
||||
null,
|
||||
|
||||
@@ -55,13 +55,16 @@
|
||||
</repair-steps>
|
||||
|
||||
<commands>
|
||||
<command>OCA\DAV\Command\ClearCalendarUnshares</command>
|
||||
<command>OCA\DAV\Command\CreateAddressBook</command>
|
||||
<command>OCA\DAV\Command\CreateCalendar</command>
|
||||
<command>OCA\DAV\Command\CreateSubscription</command>
|
||||
<command>OCA\DAV\Command\DeleteCalendar</command>
|
||||
<command>OCA\DAV\Command\DeleteSubscription</command>
|
||||
<command>OCA\DAV\Command\ExportCalendar</command>
|
||||
<command>OCA\DAV\Command\FixCalendarSyncCommand</command>
|
||||
<command>OCA\DAV\Command\ListAddressbooks</command>
|
||||
<command>OCA\DAV\Command\ListCalendarShares</command>
|
||||
<command>OCA\DAV\Command\ListCalendars</command>
|
||||
<command>OCA\DAV\Command\ListSubscriptions</command>
|
||||
<command>OCA\DAV\Command\MoveCalendar</command>
|
||||
|
||||
@@ -68,7 +68,7 @@ $requestUri = Server::get(IRequest::class)->getRequestUri();
|
||||
$linkCheckPlugin = new PublicLinkCheckPlugin();
|
||||
$filesDropPlugin = new FilesDropPlugin();
|
||||
|
||||
$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
|
||||
$server = $serverFactory->createServer(false, $baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
|
||||
$isAjax = in_array('XMLHttpRequest', explode(',', $_SERVER['HTTP_X_REQUESTED_WITH'] ?? ''));
|
||||
/** @var FederatedShareProvider $shareProvider */
|
||||
$federatedShareProvider = Server::get(FederatedShareProvider::class);
|
||||
|
||||
@@ -68,7 +68,7 @@ $authPlugin->addBackend($bearerAuthPlugin);
|
||||
|
||||
$requestUri = Server::get(IRequest::class)->getRequestUri();
|
||||
|
||||
$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function () {
|
||||
$server = $serverFactory->createServer(false, $baseuri, $requestUri, $authPlugin, function () {
|
||||
// use the view for the logged in user
|
||||
return Filesystem::getView();
|
||||
});
|
||||
|
||||
@@ -14,12 +14,15 @@ use OCA\DAV\Files\Sharing\FilesDropPlugin;
|
||||
use OCA\DAV\Files\Sharing\PublicLinkCheckPlugin;
|
||||
use OCA\DAV\Storage\PublicOwnerWrapper;
|
||||
use OCA\DAV\Storage\PublicShareWrapper;
|
||||
use OCA\DAV\Upload\ChunkingPlugin;
|
||||
use OCA\DAV\Upload\ChunkingV2Plugin;
|
||||
use OCA\FederatedFileSharing\FederatedShareProvider;
|
||||
use OCP\BeforeSabrePubliclyLoadedEvent;
|
||||
use OCP\Constants;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Mount\IMountManager;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IPreview;
|
||||
@@ -75,12 +78,8 @@ $serverFactory = new ServerFactory(
|
||||
$linkCheckPlugin = new PublicLinkCheckPlugin();
|
||||
$filesDropPlugin = new FilesDropPlugin();
|
||||
|
||||
// Define root url with /public.php/dav/files/TOKEN
|
||||
/** @var string $baseuri defined in public.php */
|
||||
preg_match('/(^files\/[a-z0-9-_]+)/i', substr($requestUri, strlen($baseuri)), $match);
|
||||
$baseuri = $baseuri . $match[0];
|
||||
|
||||
$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
|
||||
$server = $serverFactory->createServer(true, $baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
|
||||
// GET must be allowed for e.g. showing images and allowing Zip downloads
|
||||
if ($server->httpRequest->getMethod() !== 'GET') {
|
||||
// If this is *not* a GET request we only allow access to public DAV from AJAX or when Server2Server is allowed
|
||||
@@ -142,6 +141,8 @@ $server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, funct
|
||||
|
||||
$server->addPlugin($linkCheckPlugin);
|
||||
$server->addPlugin($filesDropPlugin);
|
||||
$server->addPlugin(new ChunkingV2Plugin(Server::get(ICacheFactory::class)));
|
||||
$server->addPlugin(new ChunkingPlugin());
|
||||
|
||||
// allow setup of additional plugins
|
||||
$event = new BeforeSabrePubliclyLoadedEvent($server);
|
||||
|
||||
@@ -16,6 +16,7 @@ return array(
|
||||
'OCA\\DAV\\BackgroundJob\\CalendarRetentionJob' => $baseDir . '/../lib/BackgroundJob/CalendarRetentionJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => $baseDir . '/../lib/BackgroundJob/CleanupDirectLinksJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => $baseDir . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\CleanupOrphanedChildrenJob' => $baseDir . '/../lib/BackgroundJob/CleanupOrphanedChildrenJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\DeleteOutdatedSchedulingObjects' => $baseDir . '/../lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php',
|
||||
'OCA\\DAV\\BackgroundJob\\EventReminderJob' => $baseDir . '/../lib/BackgroundJob/EventReminderJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => $baseDir . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
|
||||
@@ -63,6 +64,7 @@ return array(
|
||||
'OCA\\DAV\\CalDAV\\EventReader' => $baseDir . '/../lib/CalDAV/EventReader.php',
|
||||
'OCA\\DAV\\CalDAV\\EventReaderRDate' => $baseDir . '/../lib/CalDAV/EventReaderRDate.php',
|
||||
'OCA\\DAV\\CalDAV\\EventReaderRRule' => $baseDir . '/../lib/CalDAV/EventReaderRRule.php',
|
||||
'OCA\\DAV\\CalDAV\\Export\\ExportService' => $baseDir . '/../lib/CalDAV/Export/ExportService.php',
|
||||
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => $baseDir . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
|
||||
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
|
||||
'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php',
|
||||
@@ -153,13 +155,16 @@ return array(
|
||||
'OCA\\DAV\\CardDAV\\UserAddressBooks' => $baseDir . '/../lib/CardDAV/UserAddressBooks.php',
|
||||
'OCA\\DAV\\CardDAV\\Validation\\CardDavValidatePlugin' => $baseDir . '/../lib/CardDAV/Validation/CardDavValidatePlugin.php',
|
||||
'OCA\\DAV\\CardDAV\\Xml\\Groups' => $baseDir . '/../lib/CardDAV/Xml/Groups.php',
|
||||
'OCA\\DAV\\Command\\ClearCalendarUnshares' => $baseDir . '/../lib/Command/ClearCalendarUnshares.php',
|
||||
'OCA\\DAV\\Command\\CreateAddressBook' => $baseDir . '/../lib/Command/CreateAddressBook.php',
|
||||
'OCA\\DAV\\Command\\CreateCalendar' => $baseDir . '/../lib/Command/CreateCalendar.php',
|
||||
'OCA\\DAV\\Command\\CreateSubscription' => $baseDir . '/../lib/Command/CreateSubscription.php',
|
||||
'OCA\\DAV\\Command\\DeleteCalendar' => $baseDir . '/../lib/Command/DeleteCalendar.php',
|
||||
'OCA\\DAV\\Command\\DeleteSubscription' => $baseDir . '/../lib/Command/DeleteSubscription.php',
|
||||
'OCA\\DAV\\Command\\ExportCalendar' => $baseDir . '/../lib/Command/ExportCalendar.php',
|
||||
'OCA\\DAV\\Command\\FixCalendarSyncCommand' => $baseDir . '/../lib/Command/FixCalendarSyncCommand.php',
|
||||
'OCA\\DAV\\Command\\ListAddressbooks' => $baseDir . '/../lib/Command/ListAddressbooks.php',
|
||||
'OCA\\DAV\\Command\\ListCalendarShares' => $baseDir . '/../lib/Command/ListCalendarShares.php',
|
||||
'OCA\\DAV\\Command\\ListCalendars' => $baseDir . '/../lib/Command/ListCalendars.php',
|
||||
'OCA\\DAV\\Command\\ListSubscriptions' => $baseDir . '/../lib/Command/ListSubscriptions.php',
|
||||
'OCA\\DAV\\Command\\MoveCalendar' => $baseDir . '/../lib/Command/MoveCalendar.php',
|
||||
@@ -281,6 +286,7 @@ return array(
|
||||
'OCA\\DAV\\Files\\RootCollection' => $baseDir . '/../lib/Files/RootCollection.php',
|
||||
'OCA\\DAV\\Files\\Sharing\\FilesDropPlugin' => $baseDir . '/../lib/Files/Sharing/FilesDropPlugin.php',
|
||||
'OCA\\DAV\\Files\\Sharing\\PublicLinkCheckPlugin' => $baseDir . '/../lib/Files/Sharing/PublicLinkCheckPlugin.php',
|
||||
'OCA\\DAV\\Files\\Sharing\\RootCollection' => $baseDir . '/../lib/Files/Sharing/RootCollection.php',
|
||||
'OCA\\DAV\\Listener\\ActivityUpdaterListener' => $baseDir . '/../lib/Listener/ActivityUpdaterListener.php',
|
||||
'OCA\\DAV\\Listener\\AddMissingIndicesListener' => $baseDir . '/../lib/Listener/AddMissingIndicesListener.php',
|
||||
'OCA\\DAV\\Listener\\AddressbookListener' => $baseDir . '/../lib/Listener/AddressbookListener.php',
|
||||
|
||||
@@ -31,6 +31,7 @@ class ComposerStaticInitDAV
|
||||
'OCA\\DAV\\BackgroundJob\\CalendarRetentionJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CalendarRetentionJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectLinksJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\CleanupOrphanedChildrenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupOrphanedChildrenJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\DeleteOutdatedSchedulingObjects' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php',
|
||||
'OCA\\DAV\\BackgroundJob\\EventReminderJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/EventReminderJob.php',
|
||||
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
|
||||
@@ -78,6 +79,7 @@ class ComposerStaticInitDAV
|
||||
'OCA\\DAV\\CalDAV\\EventReader' => __DIR__ . '/..' . '/../lib/CalDAV/EventReader.php',
|
||||
'OCA\\DAV\\CalDAV\\EventReaderRDate' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRDate.php',
|
||||
'OCA\\DAV\\CalDAV\\EventReaderRRule' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRRule.php',
|
||||
'OCA\\DAV\\CalDAV\\Export\\ExportService' => __DIR__ . '/..' . '/../lib/CalDAV/Export/ExportService.php',
|
||||
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => __DIR__ . '/..' . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
|
||||
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
|
||||
'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php',
|
||||
@@ -168,13 +170,16 @@ class ComposerStaticInitDAV
|
||||
'OCA\\DAV\\CardDAV\\UserAddressBooks' => __DIR__ . '/..' . '/../lib/CardDAV/UserAddressBooks.php',
|
||||
'OCA\\DAV\\CardDAV\\Validation\\CardDavValidatePlugin' => __DIR__ . '/..' . '/../lib/CardDAV/Validation/CardDavValidatePlugin.php',
|
||||
'OCA\\DAV\\CardDAV\\Xml\\Groups' => __DIR__ . '/..' . '/../lib/CardDAV/Xml/Groups.php',
|
||||
'OCA\\DAV\\Command\\ClearCalendarUnshares' => __DIR__ . '/..' . '/../lib/Command/ClearCalendarUnshares.php',
|
||||
'OCA\\DAV\\Command\\CreateAddressBook' => __DIR__ . '/..' . '/../lib/Command/CreateAddressBook.php',
|
||||
'OCA\\DAV\\Command\\CreateCalendar' => __DIR__ . '/..' . '/../lib/Command/CreateCalendar.php',
|
||||
'OCA\\DAV\\Command\\CreateSubscription' => __DIR__ . '/..' . '/../lib/Command/CreateSubscription.php',
|
||||
'OCA\\DAV\\Command\\DeleteCalendar' => __DIR__ . '/..' . '/../lib/Command/DeleteCalendar.php',
|
||||
'OCA\\DAV\\Command\\DeleteSubscription' => __DIR__ . '/..' . '/../lib/Command/DeleteSubscription.php',
|
||||
'OCA\\DAV\\Command\\ExportCalendar' => __DIR__ . '/..' . '/../lib/Command/ExportCalendar.php',
|
||||
'OCA\\DAV\\Command\\FixCalendarSyncCommand' => __DIR__ . '/..' . '/../lib/Command/FixCalendarSyncCommand.php',
|
||||
'OCA\\DAV\\Command\\ListAddressbooks' => __DIR__ . '/..' . '/../lib/Command/ListAddressbooks.php',
|
||||
'OCA\\DAV\\Command\\ListCalendarShares' => __DIR__ . '/..' . '/../lib/Command/ListCalendarShares.php',
|
||||
'OCA\\DAV\\Command\\ListCalendars' => __DIR__ . '/..' . '/../lib/Command/ListCalendars.php',
|
||||
'OCA\\DAV\\Command\\ListSubscriptions' => __DIR__ . '/..' . '/../lib/Command/ListSubscriptions.php',
|
||||
'OCA\\DAV\\Command\\MoveCalendar' => __DIR__ . '/..' . '/../lib/Command/MoveCalendar.php',
|
||||
@@ -296,6 +301,7 @@ class ComposerStaticInitDAV
|
||||
'OCA\\DAV\\Files\\RootCollection' => __DIR__ . '/..' . '/../lib/Files/RootCollection.php',
|
||||
'OCA\\DAV\\Files\\Sharing\\FilesDropPlugin' => __DIR__ . '/..' . '/../lib/Files/Sharing/FilesDropPlugin.php',
|
||||
'OCA\\DAV\\Files\\Sharing\\PublicLinkCheckPlugin' => __DIR__ . '/..' . '/../lib/Files/Sharing/PublicLinkCheckPlugin.php',
|
||||
'OCA\\DAV\\Files\\Sharing\\RootCollection' => __DIR__ . '/..' . '/../lib/Files/Sharing/RootCollection.php',
|
||||
'OCA\\DAV\\Listener\\ActivityUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/ActivityUpdaterListener.php',
|
||||
'OCA\\DAV\\Listener\\AddMissingIndicesListener' => __DIR__ . '/..' . '/../lib/Listener/AddMissingIndicesListener.php',
|
||||
'OCA\\DAV\\Listener\\AddressbookListener' => __DIR__ . '/..' . '/../lib/Listener/AddressbookListener.php',
|
||||
|
||||
+8
-8
@@ -274,13 +274,13 @@ OC.L10N.register(
|
||||
"Name of the replacement" : "Name der Vertretung",
|
||||
"No results." : "Keine Ergebnisse",
|
||||
"Start typing." : "Mit dem Schreiben beginnen.",
|
||||
"Short absence status" : "Kurze Abwesenheits Meldung",
|
||||
"Long absence Message" : "Lange Abwesenheits Meldung",
|
||||
"Short absence status" : "Kurzer Abwesenheitsstatus",
|
||||
"Long absence Message" : "Lange Abwesenheitsnachricht",
|
||||
"Save" : "Speichern",
|
||||
"Disable absence" : "Abwesenheitmeldungen deaktivieren",
|
||||
"Failed to load availability" : "Fehler beim Laden der Verfügbarkeit",
|
||||
"Disable absence" : "Abwesenheit deaktivieren",
|
||||
"Failed to load availability" : "Verfügbarkeit konnte nicht geladen werden",
|
||||
"Saved availability" : "Verfügbarkeit gespeichert",
|
||||
"Failed to save availability" : "Fehler beim Speichern der Verfügbarkeit",
|
||||
"Failed to save availability" : "Verfügbarkeit konnte nicht gespeichert werden",
|
||||
"Time zone:" : "Zeitzone:",
|
||||
"to" : "an",
|
||||
"Delete slot" : "Slot löschen",
|
||||
@@ -291,7 +291,7 @@ OC.L10N.register(
|
||||
"Pick a end time for {dayName}" : "Eine Endezeit für {dayName} wählen",
|
||||
"Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setze den Benutzerstatus außerhalb deiner Verfügbarkeit automatisch auf \"Nicht stören\", um alle Benachrichtigungen stumm zu schalten.",
|
||||
"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.",
|
||||
"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 angibst, können andere beim Buchen einer Besprechung sehen, wann du nicht im Büro bist.",
|
||||
"Absence" : "Abwesenheit",
|
||||
"Configure your next absence period." : "Richte deinen nächsten Abwesenheitszeitraum ein.",
|
||||
"Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installiere außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinde deinen Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
|
||||
@@ -309,9 +309,9 @@ OC.L10N.register(
|
||||
"Cancel" : "Abbrechen",
|
||||
"Import" : "Importieren",
|
||||
"Error while saving settings" : "Fehler beim Speichern der Einstellungen",
|
||||
"Contact reset successfully" : "Kontakt erfolgreich zurückgesetzt",
|
||||
"Contact reset successfully" : "Kontakt zurückgesetzt",
|
||||
"Error while resetting contact" : "Fehler beim Zurücksetzen des Kontakts",
|
||||
"Contact imported successfully" : "Kontakt erfolgreich importiert",
|
||||
"Contact imported successfully" : "Kontakt importiert",
|
||||
"Error while importing contact" : "Fehler beim Import des Kontakts",
|
||||
"Example Content" : "Beispielinhalt",
|
||||
"Set example content to be created on new user first login." : "Beispielinhalte festlegen, die bei der ersten Anmeldung eines neuen Benutzers erstellt werden sollen.",
|
||||
|
||||
@@ -272,13 +272,13 @@
|
||||
"Name of the replacement" : "Name der Vertretung",
|
||||
"No results." : "Keine Ergebnisse",
|
||||
"Start typing." : "Mit dem Schreiben beginnen.",
|
||||
"Short absence status" : "Kurze Abwesenheits Meldung",
|
||||
"Long absence Message" : "Lange Abwesenheits Meldung",
|
||||
"Short absence status" : "Kurzer Abwesenheitsstatus",
|
||||
"Long absence Message" : "Lange Abwesenheitsnachricht",
|
||||
"Save" : "Speichern",
|
||||
"Disable absence" : "Abwesenheitmeldungen deaktivieren",
|
||||
"Failed to load availability" : "Fehler beim Laden der Verfügbarkeit",
|
||||
"Disable absence" : "Abwesenheit deaktivieren",
|
||||
"Failed to load availability" : "Verfügbarkeit konnte nicht geladen werden",
|
||||
"Saved availability" : "Verfügbarkeit gespeichert",
|
||||
"Failed to save availability" : "Fehler beim Speichern der Verfügbarkeit",
|
||||
"Failed to save availability" : "Verfügbarkeit konnte nicht gespeichert werden",
|
||||
"Time zone:" : "Zeitzone:",
|
||||
"to" : "an",
|
||||
"Delete slot" : "Slot löschen",
|
||||
@@ -289,7 +289,7 @@
|
||||
"Pick a end time for {dayName}" : "Eine Endezeit für {dayName} wählen",
|
||||
"Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setze den Benutzerstatus außerhalb deiner Verfügbarkeit automatisch auf \"Nicht stören\", um alle Benachrichtigungen stumm zu schalten.",
|
||||
"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.",
|
||||
"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 angibst, können andere beim Buchen einer Besprechung sehen, wann du nicht im Büro bist.",
|
||||
"Absence" : "Abwesenheit",
|
||||
"Configure your next absence period." : "Richte deinen nächsten Abwesenheitszeitraum ein.",
|
||||
"Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installiere außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinde deinen Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
|
||||
@@ -307,9 +307,9 @@
|
||||
"Cancel" : "Abbrechen",
|
||||
"Import" : "Importieren",
|
||||
"Error while saving settings" : "Fehler beim Speichern der Einstellungen",
|
||||
"Contact reset successfully" : "Kontakt erfolgreich zurückgesetzt",
|
||||
"Contact reset successfully" : "Kontakt zurückgesetzt",
|
||||
"Error while resetting contact" : "Fehler beim Zurücksetzen des Kontakts",
|
||||
"Contact imported successfully" : "Kontakt erfolgreich importiert",
|
||||
"Contact imported successfully" : "Kontakt importiert",
|
||||
"Error while importing contact" : "Fehler beim Import des Kontakts",
|
||||
"Example Content" : "Beispielinhalt",
|
||||
"Set example content to be created on new user first login." : "Beispielinhalte festlegen, die bei der ersten Anmeldung eines neuen Benutzers erstellt werden sollen.",
|
||||
|
||||
@@ -274,26 +274,26 @@ OC.L10N.register(
|
||||
"Name of the replacement" : "Name der Vertretung",
|
||||
"No results." : "Keine Ergebnisse.",
|
||||
"Start typing." : "Anfangen zu tippen.",
|
||||
"Short absence status" : "Kurzer Abwesenheitsstaus",
|
||||
"Short absence status" : "Kurzer Abwesenheitsstatus",
|
||||
"Long absence Message" : "Lange Abwesenheitsnachricht",
|
||||
"Save" : "Speichern",
|
||||
"Disable absence" : "Abwesenheit deaktivieren",
|
||||
"Failed to load availability" : "Laden der Verfügbarkeit fehlgeschlagen",
|
||||
"Failed to load availability" : "Verfügbarkeit konnte nicht geladen werden",
|
||||
"Saved availability" : "Verfügbarkeit gespeichert",
|
||||
"Failed to save availability" : "Fehler beim Speichern der Verfügbarkeit",
|
||||
"Failed to save availability" : "Verfügbarkeit konnte nicht gespeichert werden",
|
||||
"Time zone:" : "Zeitzone:",
|
||||
"to" : "an",
|
||||
"Delete slot" : "Zeitfenster löschen",
|
||||
"No working hours set" : "Arbeitsfreie Stunden gesetzt",
|
||||
"No working hours set" : "Keine Arbeitszeiten konfiguriert",
|
||||
"Add slot" : "Zeitfenster hinzufügen",
|
||||
"Weekdays" : "Wochentage",
|
||||
"Pick a start time for {dayName}" : "Eine Startzeit für {dayName} wählen",
|
||||
"Pick a end time for {dayName}" : "Eine Endezeit für {dayName} wählen",
|
||||
"Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setzen Sie den Benutzerstatus außerhalb Ihrer Verfügbarkeit automatisch auf \"Nicht stören\", um alle Benachrichtigungen stumm zu schalten.",
|
||||
"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 Sie Ihre Arbeitszeiten angeben, können Andere beim Buchen einer Besprechung sehen, wann Sie nicht im Büro sind.",
|
||||
"If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Wenn Sie Ihre Arbeitszeiten angeben, können andere beim Buchen einer Besprechung sehen, wann Sie nicht im Büro sind.",
|
||||
"Absence" : "Abwesenheit",
|
||||
"Configure your next absence period." : "Richten Sie ihren nächsten Abwesenheitszeitraum ein",
|
||||
"Configure your next absence period." : "Richten Sie ihren nächsten Abwesenheitszeitraum ein.",
|
||||
"Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installieren Sie außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinden Sie Ihren Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
|
||||
"Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stellen Sie sicher, dass Sie {emailopen}den E-Mail Server{linkclose} ordnungsgemäß eingerichtet haben.",
|
||||
"Calendar server" : "Kalender-Server",
|
||||
|
||||
@@ -272,26 +272,26 @@
|
||||
"Name of the replacement" : "Name der Vertretung",
|
||||
"No results." : "Keine Ergebnisse.",
|
||||
"Start typing." : "Anfangen zu tippen.",
|
||||
"Short absence status" : "Kurzer Abwesenheitsstaus",
|
||||
"Short absence status" : "Kurzer Abwesenheitsstatus",
|
||||
"Long absence Message" : "Lange Abwesenheitsnachricht",
|
||||
"Save" : "Speichern",
|
||||
"Disable absence" : "Abwesenheit deaktivieren",
|
||||
"Failed to load availability" : "Laden der Verfügbarkeit fehlgeschlagen",
|
||||
"Failed to load availability" : "Verfügbarkeit konnte nicht geladen werden",
|
||||
"Saved availability" : "Verfügbarkeit gespeichert",
|
||||
"Failed to save availability" : "Fehler beim Speichern der Verfügbarkeit",
|
||||
"Failed to save availability" : "Verfügbarkeit konnte nicht gespeichert werden",
|
||||
"Time zone:" : "Zeitzone:",
|
||||
"to" : "an",
|
||||
"Delete slot" : "Zeitfenster löschen",
|
||||
"No working hours set" : "Arbeitsfreie Stunden gesetzt",
|
||||
"No working hours set" : "Keine Arbeitszeiten konfiguriert",
|
||||
"Add slot" : "Zeitfenster hinzufügen",
|
||||
"Weekdays" : "Wochentage",
|
||||
"Pick a start time for {dayName}" : "Eine Startzeit für {dayName} wählen",
|
||||
"Pick a end time for {dayName}" : "Eine Endezeit für {dayName} wählen",
|
||||
"Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Setzen Sie den Benutzerstatus außerhalb Ihrer Verfügbarkeit automatisch auf \"Nicht stören\", um alle Benachrichtigungen stumm zu schalten.",
|
||||
"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 Sie Ihre Arbeitszeiten angeben, können Andere beim Buchen einer Besprechung sehen, wann Sie nicht im Büro sind.",
|
||||
"If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Wenn Sie Ihre Arbeitszeiten angeben, können andere beim Buchen einer Besprechung sehen, wann Sie nicht im Büro sind.",
|
||||
"Absence" : "Abwesenheit",
|
||||
"Configure your next absence period." : "Richten Sie ihren nächsten Abwesenheitszeitraum ein",
|
||||
"Configure your next absence period." : "Richten Sie ihren nächsten Abwesenheitszeitraum ein.",
|
||||
"Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installieren Sie außerdem die {calendarappstoreopen}Kalender-App{linkclose} oder {calendardocopen}verbinden Sie Ihren Desktop & Mobilgerät zur Synchronisierung ↗{linkclose}.",
|
||||
"Please make sure to properly set up {emailopen}the email server{linkclose}." : "Bitte stellen Sie sicher, dass Sie {emailopen}den E-Mail Server{linkclose} ordnungsgemäß eingerichtet haben.",
|
||||
"Calendar server" : "Kalender-Server",
|
||||
|
||||
@@ -68,11 +68,27 @@ OC.L10N.register(
|
||||
"Description: %s" : "Kirjeldus: %s",
|
||||
"Where: %s" : "Kus: %s",
|
||||
"%1$s via %2$s" : "%1$s %2$s kaudu",
|
||||
"Cancelled: %1$s" : "Tühistatud: %1$s",
|
||||
"\"%1$s\" has been canceled" : "„%1$s“ on tühistatud",
|
||||
"Re: %1$s" : "Re: %1$s",
|
||||
"%1$s has accepted your invitation" : "„%1$s“ on nõustunud sinu kutsega",
|
||||
"%1$s has tentatively accepted your invitation" : "„%1$s“ on esialgselt nõustunud sinu kutsega",
|
||||
"%1$s has declined your invitation" : "„%1$s“ on sinu kutsest keeldunud",
|
||||
"%1$s has responded to your invitation" : "„%1$s“ on vastanud sinu kutsele",
|
||||
"Invitation updated: %1$s" : "Kutse on uuendatud: %1$s",
|
||||
"%1$s updated the event \"%2$s\"" : "„%1$s“ uuendas sündmust „%2$s“",
|
||||
"Invitation: %1$s" : "Kutse: %1$s",
|
||||
"%1$s would like to invite you to \"%2$s\"" : "„%1$s“ soovib saata sulle „%2$s“ kutset",
|
||||
"Organizer:" : "Korraldaja:",
|
||||
"Attendees:" : "Osalejad:",
|
||||
"Title:" : "Pealkiri:",
|
||||
"When:" : "Millal:",
|
||||
"Location:" : "Asukoht:",
|
||||
"Link:" : "Link:",
|
||||
"Accept" : "Nõustu",
|
||||
"Decline" : "Keeldu",
|
||||
"More options …" : "Täiendavad valikud…",
|
||||
"More options at %s" : "Lisavalikud: %s",
|
||||
"Monday" : "Esmaspäev",
|
||||
"Tuesday" : "Teisipäev",
|
||||
"Wednesday" : "Kolmapäev",
|
||||
@@ -99,6 +115,13 @@ OC.L10N.register(
|
||||
"Fifth" : "Viies",
|
||||
"Last" : "Viimane",
|
||||
"Contacts" : "Kontaktid",
|
||||
"You created address book {addressbook}" : "Sa lõid aadressiraamatu „{addressbook}“",
|
||||
"{actor} deleted address book {addressbook}" : "„{actor}“ kustutas „{addressbook}“ aadressiraamatu",
|
||||
"You deleted address book {addressbook}" : "Sa kustutasid aadressiraamatu „{addressbook}“",
|
||||
"You shared address book {addressbook} with {user}" : "Sa jagasid „{addressbook}“ aadressiraamatut kasutajaga „{user}“",
|
||||
"You deleted contact {card} from address book {addressbook}" : "Sa kustutasid „{card}“ kontakti „{addressbook}“ aadressiraamatus",
|
||||
"You updated contact {card} in address book {addressbook}" : "Sa uuendasid „{card}“ kontakti „{addressbook}“ aadressiraamatus",
|
||||
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Kontakti</strong> või <strong>aadressiraamatut</strong> muudeti",
|
||||
"Accounts" : "Kasutajakontod",
|
||||
"System address book which holds all accounts" : "Süsteemne aadressiraamat, kus leiduvad kõik kasutajakontod",
|
||||
"File is not updatable: %1$s" : "Fail pole uuendatav: %1$s",
|
||||
@@ -113,12 +136,16 @@ OC.L10N.register(
|
||||
"Failed to unlink: %1$s" : "Lingi eemaldamine ei õnnestunud: %1$s",
|
||||
"Failed to write file contents: %1$s" : "Faili sisu salvestamine ei õnnestunud: %1$s",
|
||||
"File not found: %1$s" : "Faili ei leidu: %1$s",
|
||||
"Invalid target path" : "Vigane sihtasukoht",
|
||||
"System is in maintenance mode." : "Server on hooldusrežiimis.",
|
||||
"Upgrade needed" : "Uuendus on vajalik",
|
||||
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Selleks, et sinu %s toimiks iOS-is/macOS-is CalDAV-i ja CardDAV-iga peab https olema seadistatud.",
|
||||
"Configures a CalDAV account" : "Seadistab CalDAV-i kasutajakonto",
|
||||
"Configures a CardDAV account" : "Seadistab CardDAV-i kasutajakonto",
|
||||
"Events" : "Sündmused",
|
||||
"Untitled task" : "Ilma nimeta pealkiri",
|
||||
"Completed on %s" : "Lõpetatud %s",
|
||||
"Due on %s by %s" : "Tähtaeg: %s, täitjaks %s",
|
||||
"Due on %s" : "Tähtaeg: %s",
|
||||
"System Address Book" : "Süsteemne aadressiraamat",
|
||||
"The system address book contains contact information for all users in your instance." : "Süsteemses aadressiraamatus leiduvad kõikde selle serveri kasutajate kontaktteave.",
|
||||
@@ -137,6 +164,8 @@ OC.L10N.register(
|
||||
"Failed to clear your absence settings" : "Sinu äraoleku seadistuste eemaldamine ei õnnestunud",
|
||||
"First day" : "Esimene päev",
|
||||
"Last day (inclusive)" : "Viimane päev (kaasaarvatud)",
|
||||
"Out of office replacement (optional)" : "Asendaja äraoleku ajaks (valikuline)",
|
||||
"Name of the replacement" : "Asendaja nimi",
|
||||
"No results." : "Vasteid ei leitud.",
|
||||
"Start typing." : "Alusta kirjutamist.",
|
||||
"Short absence status" : "Äraoleku lühinimi",
|
||||
@@ -148,10 +177,18 @@ OC.L10N.register(
|
||||
"Failed to save availability" : "Saadavuse salvestamine ei õnnestunud",
|
||||
"Time zone:" : "Ajavöönd:",
|
||||
"to" : "saaja",
|
||||
"Delete slot" : "Kustuta ajavahemik",
|
||||
"No working hours set" : "Tööajad on sisestamata",
|
||||
"Add slot" : "Lisa ajavahemik",
|
||||
"Weekdays" : "Nädalapäevad",
|
||||
"Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Kõikide teavituste summutamiseks määra automaatselt kasutajale olek „Ära sega“ nendele aegadele, kus ta vaba ei ole.",
|
||||
"Availability" : "Saadavus",
|
||||
"Absence" : "Äraolek",
|
||||
"Configure your next absence period." : "Seadista järgmise äraoleku ajavahemik.",
|
||||
"Calendar server" : "Kalendriserver",
|
||||
"Send invitations to attendees" : "Saada osalejatele kutsed",
|
||||
"Automatically generate a birthday calendar" : "Koosta sünnipäevade kalender automaatselt",
|
||||
"Send notifications for events" : "Saada sündmuste teavitusi",
|
||||
"Enable notifications for events via push" : "Võta kasutusele tõuketeenustepõhised teavitused",
|
||||
"Cancel" : "Tühista",
|
||||
"Import" : "Impordi",
|
||||
|
||||
@@ -66,11 +66,27 @@
|
||||
"Description: %s" : "Kirjeldus: %s",
|
||||
"Where: %s" : "Kus: %s",
|
||||
"%1$s via %2$s" : "%1$s %2$s kaudu",
|
||||
"Cancelled: %1$s" : "Tühistatud: %1$s",
|
||||
"\"%1$s\" has been canceled" : "„%1$s“ on tühistatud",
|
||||
"Re: %1$s" : "Re: %1$s",
|
||||
"%1$s has accepted your invitation" : "„%1$s“ on nõustunud sinu kutsega",
|
||||
"%1$s has tentatively accepted your invitation" : "„%1$s“ on esialgselt nõustunud sinu kutsega",
|
||||
"%1$s has declined your invitation" : "„%1$s“ on sinu kutsest keeldunud",
|
||||
"%1$s has responded to your invitation" : "„%1$s“ on vastanud sinu kutsele",
|
||||
"Invitation updated: %1$s" : "Kutse on uuendatud: %1$s",
|
||||
"%1$s updated the event \"%2$s\"" : "„%1$s“ uuendas sündmust „%2$s“",
|
||||
"Invitation: %1$s" : "Kutse: %1$s",
|
||||
"%1$s would like to invite you to \"%2$s\"" : "„%1$s“ soovib saata sulle „%2$s“ kutset",
|
||||
"Organizer:" : "Korraldaja:",
|
||||
"Attendees:" : "Osalejad:",
|
||||
"Title:" : "Pealkiri:",
|
||||
"When:" : "Millal:",
|
||||
"Location:" : "Asukoht:",
|
||||
"Link:" : "Link:",
|
||||
"Accept" : "Nõustu",
|
||||
"Decline" : "Keeldu",
|
||||
"More options …" : "Täiendavad valikud…",
|
||||
"More options at %s" : "Lisavalikud: %s",
|
||||
"Monday" : "Esmaspäev",
|
||||
"Tuesday" : "Teisipäev",
|
||||
"Wednesday" : "Kolmapäev",
|
||||
@@ -97,6 +113,13 @@
|
||||
"Fifth" : "Viies",
|
||||
"Last" : "Viimane",
|
||||
"Contacts" : "Kontaktid",
|
||||
"You created address book {addressbook}" : "Sa lõid aadressiraamatu „{addressbook}“",
|
||||
"{actor} deleted address book {addressbook}" : "„{actor}“ kustutas „{addressbook}“ aadressiraamatu",
|
||||
"You deleted address book {addressbook}" : "Sa kustutasid aadressiraamatu „{addressbook}“",
|
||||
"You shared address book {addressbook} with {user}" : "Sa jagasid „{addressbook}“ aadressiraamatut kasutajaga „{user}“",
|
||||
"You deleted contact {card} from address book {addressbook}" : "Sa kustutasid „{card}“ kontakti „{addressbook}“ aadressiraamatus",
|
||||
"You updated contact {card} in address book {addressbook}" : "Sa uuendasid „{card}“ kontakti „{addressbook}“ aadressiraamatus",
|
||||
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Kontakti</strong> või <strong>aadressiraamatut</strong> muudeti",
|
||||
"Accounts" : "Kasutajakontod",
|
||||
"System address book which holds all accounts" : "Süsteemne aadressiraamat, kus leiduvad kõik kasutajakontod",
|
||||
"File is not updatable: %1$s" : "Fail pole uuendatav: %1$s",
|
||||
@@ -111,12 +134,16 @@
|
||||
"Failed to unlink: %1$s" : "Lingi eemaldamine ei õnnestunud: %1$s",
|
||||
"Failed to write file contents: %1$s" : "Faili sisu salvestamine ei õnnestunud: %1$s",
|
||||
"File not found: %1$s" : "Faili ei leidu: %1$s",
|
||||
"Invalid target path" : "Vigane sihtasukoht",
|
||||
"System is in maintenance mode." : "Server on hooldusrežiimis.",
|
||||
"Upgrade needed" : "Uuendus on vajalik",
|
||||
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Selleks, et sinu %s toimiks iOS-is/macOS-is CalDAV-i ja CardDAV-iga peab https olema seadistatud.",
|
||||
"Configures a CalDAV account" : "Seadistab CalDAV-i kasutajakonto",
|
||||
"Configures a CardDAV account" : "Seadistab CardDAV-i kasutajakonto",
|
||||
"Events" : "Sündmused",
|
||||
"Untitled task" : "Ilma nimeta pealkiri",
|
||||
"Completed on %s" : "Lõpetatud %s",
|
||||
"Due on %s by %s" : "Tähtaeg: %s, täitjaks %s",
|
||||
"Due on %s" : "Tähtaeg: %s",
|
||||
"System Address Book" : "Süsteemne aadressiraamat",
|
||||
"The system address book contains contact information for all users in your instance." : "Süsteemses aadressiraamatus leiduvad kõikde selle serveri kasutajate kontaktteave.",
|
||||
@@ -135,6 +162,8 @@
|
||||
"Failed to clear your absence settings" : "Sinu äraoleku seadistuste eemaldamine ei õnnestunud",
|
||||
"First day" : "Esimene päev",
|
||||
"Last day (inclusive)" : "Viimane päev (kaasaarvatud)",
|
||||
"Out of office replacement (optional)" : "Asendaja äraoleku ajaks (valikuline)",
|
||||
"Name of the replacement" : "Asendaja nimi",
|
||||
"No results." : "Vasteid ei leitud.",
|
||||
"Start typing." : "Alusta kirjutamist.",
|
||||
"Short absence status" : "Äraoleku lühinimi",
|
||||
@@ -146,10 +175,18 @@
|
||||
"Failed to save availability" : "Saadavuse salvestamine ei õnnestunud",
|
||||
"Time zone:" : "Ajavöönd:",
|
||||
"to" : "saaja",
|
||||
"Delete slot" : "Kustuta ajavahemik",
|
||||
"No working hours set" : "Tööajad on sisestamata",
|
||||
"Add slot" : "Lisa ajavahemik",
|
||||
"Weekdays" : "Nädalapäevad",
|
||||
"Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Kõikide teavituste summutamiseks määra automaatselt kasutajale olek „Ära sega“ nendele aegadele, kus ta vaba ei ole.",
|
||||
"Availability" : "Saadavus",
|
||||
"Absence" : "Äraolek",
|
||||
"Configure your next absence period." : "Seadista järgmise äraoleku ajavahemik.",
|
||||
"Calendar server" : "Kalendriserver",
|
||||
"Send invitations to attendees" : "Saada osalejatele kutsed",
|
||||
"Automatically generate a birthday calendar" : "Koosta sünnipäevade kalender automaatselt",
|
||||
"Send notifications for events" : "Saada sündmuste teavitusi",
|
||||
"Enable notifications for events via push" : "Võta kasutusele tõuketeenustepõhised teavitused",
|
||||
"Cancel" : "Tühista",
|
||||
"Import" : "Impordi",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\BackgroundJob;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\BackgroundJob\QueuedJob;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class CleanupOrphanedChildrenJob extends QueuedJob {
|
||||
public const ARGUMENT_CHILD_TABLE = 'childTable';
|
||||
public const ARGUMENT_PARENT_TABLE = 'parentTable';
|
||||
public const ARGUMENT_PARENT_ID = 'parentId';
|
||||
public const ARGUMENT_LOG_MESSAGE = 'logMessage';
|
||||
|
||||
private const BATCH_SIZE = 1000;
|
||||
|
||||
public function __construct(
|
||||
ITimeFactory $time,
|
||||
private readonly IDBConnection $connection,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly IJobList $jobList,
|
||||
) {
|
||||
parent::__construct($time);
|
||||
}
|
||||
|
||||
protected function run($argument): void {
|
||||
$childTable = $argument[self::ARGUMENT_CHILD_TABLE];
|
||||
$parentTable = $argument[self::ARGUMENT_PARENT_TABLE];
|
||||
$parentId = $argument[self::ARGUMENT_PARENT_ID];
|
||||
$logMessage = $argument[self::ARGUMENT_LOG_MESSAGE];
|
||||
|
||||
$orphanCount = $this->cleanUpOrphans($childTable, $parentTable, $parentId);
|
||||
$this->logger->debug(sprintf($logMessage, $orphanCount));
|
||||
|
||||
// Requeue if there might be more orphans
|
||||
if ($orphanCount >= self::BATCH_SIZE) {
|
||||
$this->jobList->add(self::class, $argument);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanUpOrphans(
|
||||
string $childTable,
|
||||
string $parentTable,
|
||||
string $parentId,
|
||||
): int {
|
||||
// We can't merge both queries into a single one here as DELETEing from a table while
|
||||
// SELECTing it in a sub query is not supported by Oracle DB.
|
||||
// Ref https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/delete.html#idm46006185488144
|
||||
|
||||
$selectQb = $this->connection->getQueryBuilder();
|
||||
|
||||
$selectQb->select('c.id')
|
||||
->from($childTable, 'c')
|
||||
->leftJoin('c', $parentTable, 'p', $selectQb->expr()->eq('c.' . $parentId, 'p.id'))
|
||||
->where($selectQb->expr()->isNull('p.id'))
|
||||
->setMaxResults(self::BATCH_SIZE);
|
||||
|
||||
if (\in_array($parentTable, ['calendars', 'calendarsubscriptions'], true)) {
|
||||
$calendarType = $parentTable === 'calendarsubscriptions' ? CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION : CalDavBackend::CALENDAR_TYPE_CALENDAR;
|
||||
$selectQb->andWhere($selectQb->expr()->eq('c.calendartype', $selectQb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
|
||||
}
|
||||
|
||||
$result = $selectQb->executeQuery();
|
||||
$rows = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
if (empty($rows)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$orphanItems = array_map(static fn ($row) => $row['id'], $rows);
|
||||
$deleteQb = $this->connection->getQueryBuilder();
|
||||
$deleteQb->delete($childTable)
|
||||
->where($deleteQb->expr()->in('id', $deleteQb->createNamedParameter($orphanItems, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
$deleteQb->executeStatement();
|
||||
|
||||
return count($orphanItems);
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,12 @@ declare(strict_types=1);
|
||||
namespace OCA\DAV\CalDAV;
|
||||
|
||||
use OCP\Calendar\ICalendar;
|
||||
use OCP\Calendar\ICalendarIsEnabled;
|
||||
use OCP\Calendar\ICalendarIsShared;
|
||||
use OCP\Calendar\ICalendarIsWritable;
|
||||
use OCP\Constants;
|
||||
|
||||
class CachedSubscriptionImpl implements ICalendar, ICalendarIsShared, ICalendarIsWritable {
|
||||
class CachedSubscriptionImpl implements ICalendar, ICalendarIsEnabled, ICalendarIsShared, ICalendarIsWritable {
|
||||
|
||||
public function __construct(
|
||||
private CachedSubscription $calendar,
|
||||
@@ -86,6 +87,13 @@ class CachedSubscriptionImpl implements ICalendar, ICalendarIsShared, ICalendarI
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 32.0.0
|
||||
*/
|
||||
public function isEnabled(): bool {
|
||||
return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true;
|
||||
}
|
||||
|
||||
public function isWritable(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace OCA\DAV\CalDAV;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Generator;
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\CalDAV\Sharing\Backend;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
@@ -28,6 +29,7 @@ use OCA\DAV\Events\SubscriptionCreatedEvent;
|
||||
use OCA\DAV\Events\SubscriptionDeletedEvent;
|
||||
use OCA\DAV\Events\SubscriptionUpdatedEvent;
|
||||
use OCP\AppFramework\Db\TTransactional;
|
||||
use OCP\Calendar\CalendarExportOptions;
|
||||
use OCP\Calendar\Events\CalendarObjectCreatedEvent;
|
||||
use OCP\Calendar\Events\CalendarObjectDeletedEvent;
|
||||
use OCP\Calendar\Events\CalendarObjectMovedEvent;
|
||||
@@ -88,6 +90,19 @@ use function time;
|
||||
* Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
|
||||
*
|
||||
* @package OCA\DAV\CalDAV
|
||||
*
|
||||
* @psalm-type CalendarInfo = array{
|
||||
* id: int,
|
||||
* uri: string,
|
||||
* principaluri: string,
|
||||
* '{http://calendarserver.org/ns/}getctag': string,
|
||||
* '{http://sabredav.org/ns}sync-token': int,
|
||||
* '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet,
|
||||
* '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': \Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp,
|
||||
* '{DAV:}displayname': string,
|
||||
* '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string,
|
||||
* '{http://nextcloud.com/ns}owner-displayname': string,
|
||||
* }
|
||||
*/
|
||||
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
|
||||
use TTransactional;
|
||||
@@ -372,7 +387,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
||||
$subSelect->select('resourceid')
|
||||
->from('dav_shares', 'd')
|
||||
->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
|
||||
->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY));
|
||||
->andWhere($subSelect->expr()->eq('d.principaluri', $select->createNamedParameter($principalUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
|
||||
|
||||
$select->select($fields)
|
||||
->from('dav_shares', 's')
|
||||
@@ -649,7 +664,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null
|
||||
* @psalm-return CalendarInfo|null
|
||||
* @return array|null
|
||||
*/
|
||||
public function getCalendarById(int $calendarId): ?array {
|
||||
$fields = array_column($this->propertyMap, 0);
|
||||
@@ -987,6 +1003,44 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
||||
}, $this->db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all calendar entries as a stream of data
|
||||
*
|
||||
* @since 32.0.0
|
||||
*
|
||||
* @return Generator<array>
|
||||
*/
|
||||
public function exportCalendar(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR, ?CalendarExportOptions $options = null): Generator {
|
||||
// extract options
|
||||
$rangeStart = $options?->getRangeStart();
|
||||
$rangeCount = $options?->getRangeCount();
|
||||
// construct query
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('calendarobjects')
|
||||
->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
|
||||
->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
|
||||
->andWhere($qb->expr()->isNull('deleted_at'));
|
||||
if ($rangeStart !== null) {
|
||||
$qb->andWhere($qb->expr()->gt('uid', $qb->createNamedParameter($rangeStart)));
|
||||
}
|
||||
if ($rangeCount !== null) {
|
||||
$qb->setMaxResults($rangeCount);
|
||||
}
|
||||
if ($rangeStart !== null || $rangeCount !== null) {
|
||||
$qb->orderBy('uid', 'ASC');
|
||||
}
|
||||
$rs = $qb->executeQuery();
|
||||
// iterate through results
|
||||
try {
|
||||
while (($row = $rs->fetch()) !== false) {
|
||||
yield $row;
|
||||
}
|
||||
} finally {
|
||||
$rs->closeCursor();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all calendar objects with limited metadata for a calendar
|
||||
*
|
||||
@@ -1429,37 +1483,40 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
||||
/**
|
||||
* Moves a calendar object from calendar to calendar.
|
||||
*
|
||||
* @param int $sourceCalendarId
|
||||
* @param string $sourcePrincipalUri
|
||||
* @param int $sourceObjectId
|
||||
* @param string $targetPrincipalUri
|
||||
* @param int $targetCalendarId
|
||||
* @param int $objectId
|
||||
* @param string $oldPrincipalUri
|
||||
* @param string $newPrincipalUri
|
||||
* @param string $tragetObjectUri
|
||||
* @param int $calendarType
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
|
||||
public function moveCalendarObject(string $sourcePrincipalUri, int $sourceObjectId, string $targetPrincipalUri, int $targetCalendarId, string $tragetObjectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
|
||||
$this->cachedObjects = [];
|
||||
return $this->atomic(function () use ($sourceCalendarId, $targetCalendarId, $objectId, $oldPrincipalUri, $newPrincipalUri, $calendarType) {
|
||||
$object = $this->getCalendarObjectById($oldPrincipalUri, $objectId);
|
||||
return $this->atomic(function () use ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) {
|
||||
$object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId);
|
||||
if (empty($object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sourceCalendarId = $object['calendarid'];
|
||||
$sourceObjectUri = $object['uri'];
|
||||
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->update('calendarobjects')
|
||||
->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
|
||||
->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
|
||||
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
|
||||
->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR))
|
||||
->where($query->expr()->eq('id', $query->createNamedParameter($sourceObjectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
|
||||
->executeStatement();
|
||||
|
||||
$this->purgeProperties($sourceCalendarId, $objectId);
|
||||
$this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
|
||||
$this->purgeProperties($sourceCalendarId, $sourceObjectId);
|
||||
$this->updateProperties($targetCalendarId, $tragetObjectUri, $object['calendardata'], $calendarType);
|
||||
|
||||
$this->addChanges($sourceCalendarId, [$object['uri']], 3, $calendarType);
|
||||
$this->addChanges($targetCalendarId, [$object['uri']], 1, $calendarType);
|
||||
$this->addChanges($sourceCalendarId, [$sourceObjectUri], 3, $calendarType);
|
||||
$this->addChanges($targetCalendarId, [$tragetObjectUri], 1, $calendarType);
|
||||
|
||||
$object = $this->getCalendarObjectById($newPrincipalUri, $objectId);
|
||||
$object = $this->getCalendarObjectById($targetPrincipalUri, $sourceObjectId);
|
||||
// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
|
||||
if (empty($object)) {
|
||||
return false;
|
||||
@@ -3628,4 +3685,26 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
||||
->where($cmd->expr()->eq('uid', $cmd->createNamedParameter($eventId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
|
||||
$cmd->executeStatement();
|
||||
}
|
||||
|
||||
public function unshare(IShareable $shareable, string $principal): void {
|
||||
$this->atomic(function () use ($shareable, $principal): void {
|
||||
$calendarData = $this->getCalendarById($shareable->getResourceId());
|
||||
if ($calendarData === null) {
|
||||
throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $shareable->getResourceId());
|
||||
}
|
||||
|
||||
$oldShares = $this->getShares($shareable->getResourceId());
|
||||
$unshare = $this->calendarSharingBackend->unshare($shareable, $principal);
|
||||
|
||||
if ($unshare) {
|
||||
$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent(
|
||||
$shareable->getResourceId(),
|
||||
$calendarData,
|
||||
$oldShares,
|
||||
[],
|
||||
[$principal]
|
||||
));
|
||||
}
|
||||
}, $this->db);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,12 +214,8 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) &&
|
||||
$this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) {
|
||||
$principal = 'principal:' . parent::getOwner();
|
||||
$this->caldavBackend->updateShares($this, [], [
|
||||
$principal
|
||||
]);
|
||||
if ($this->isShared()) {
|
||||
$this->caldavBackend->unshare($this, 'principal:' . $this->getPrincipalURI());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -391,9 +387,14 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
||||
if (!($sourceNode instanceof CalendarObject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->caldavBackend->moveCalendarObject($sourceNode->getCalendarId(), (int)$this->calendarInfo['id'], $sourceNode->getId(), $sourceNode->getOwner(), $this->getOwner());
|
||||
return $this->caldavBackend->moveCalendarObject(
|
||||
$sourceNode->getOwner(),
|
||||
$sourceNode->getId(),
|
||||
$this->getOwner(),
|
||||
$this->getResourceId(),
|
||||
$targetName,
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
|
||||
return false;
|
||||
|
||||
@@ -8,9 +8,15 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace OCA\DAV\CalDAV;
|
||||
|
||||
use Generator;
|
||||
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
|
||||
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
|
||||
use OCP\Calendar\CalendarExportOptions;
|
||||
use OCP\Calendar\Exceptions\CalendarException;
|
||||
use OCP\Calendar\ICalendarExport;
|
||||
use OCP\Calendar\ICalendarIsEnabled;
|
||||
use OCP\Calendar\ICalendarIsShared;
|
||||
use OCP\Calendar\ICalendarIsWritable;
|
||||
use OCP\Calendar\ICreateFromString;
|
||||
use OCP\Calendar\IHandleImipMessage;
|
||||
use OCP\Constants;
|
||||
@@ -24,7 +30,7 @@ use Sabre\VObject\Property;
|
||||
use Sabre\VObject\Reader;
|
||||
use function Sabre\Uri\split as uriSplit;
|
||||
|
||||
class CalendarImpl implements ICreateFromString, IHandleImipMessage {
|
||||
class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIsWritable, ICalendarIsShared, ICalendarExport, ICalendarIsEnabled {
|
||||
public function __construct(
|
||||
private Calendar $calendar,
|
||||
/** @var array<string, mixed> */
|
||||
@@ -131,6 +137,13 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 32.0.0
|
||||
*/
|
||||
public function isEnabled(): bool {
|
||||
return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 31.0.0
|
||||
*/
|
||||
@@ -257,4 +270,27 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
|
||||
public function getInvitationResponseServer(): InvitationResponseServer {
|
||||
return new InvitationResponseServer(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export objects
|
||||
*
|
||||
* @since 32.0.0
|
||||
*
|
||||
* @return Generator<mixed, \Sabre\VObject\Component\VCalendar, mixed, mixed>
|
||||
*/
|
||||
public function export(?CalendarExportOptions $options = null): Generator {
|
||||
foreach (
|
||||
$this->backend->exportCalendar(
|
||||
$this->calendarInfo['id'],
|
||||
$this->backend::CALENDAR_TYPE_CALENDAR,
|
||||
$options
|
||||
) as $event
|
||||
) {
|
||||
$vObject = Reader::read($event['calendardata']);
|
||||
if ($vObject instanceof VCalendar) {
|
||||
yield $vObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace OCA\DAV\CalDAV;
|
||||
|
||||
use OCA\DAV\Db\Property;
|
||||
use OCA\DAV\Db\PropertyMapper;
|
||||
use OCP\Calendar\ICalendarProvider;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
@@ -20,6 +22,7 @@ class CalendarProvider implements ICalendarProvider {
|
||||
private IL10N $l10n,
|
||||
private IConfig $config,
|
||||
private LoggerInterface $logger,
|
||||
private PropertyMapper $propertyMapper,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -35,6 +38,7 @@ class CalendarProvider implements ICalendarProvider {
|
||||
|
||||
$iCalendars = [];
|
||||
foreach ($calendarInfos as $calendarInfo) {
|
||||
$calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri']));
|
||||
$calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger);
|
||||
$iCalendars[] = new CalendarImpl(
|
||||
$calendar,
|
||||
@@ -44,4 +48,23 @@ class CalendarProvider implements ICalendarProvider {
|
||||
}
|
||||
return $iCalendars;
|
||||
}
|
||||
|
||||
public function getAdditionalProperties(string $principalUri, string $calendarUri): array {
|
||||
$user = str_replace('principals/users/', '', $principalUri);
|
||||
$path = 'calendars/' . $user . '/' . $calendarUri;
|
||||
|
||||
$properties = $this->propertyMapper->findPropertiesByPath($user, $path);
|
||||
|
||||
$list = [];
|
||||
foreach ($properties as $property) {
|
||||
if ($property instanceof Property) {
|
||||
$list[$property->getPropertyname()] = match ($property->getPropertyname()) {
|
||||
'{http://owncloud.org/ns}calendar-enabled' => (bool)$property->getPropertyvalue(),
|
||||
default => $property->getPropertyvalue()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\DAV\CalDAV\Export;
|
||||
|
||||
use Generator;
|
||||
use OCP\Calendar\CalendarExportOptions;
|
||||
use OCP\Calendar\ICalendarExport;
|
||||
use OCP\ServerVersion;
|
||||
use Sabre\VObject\Component;
|
||||
use Sabre\VObject\Writer;
|
||||
|
||||
/**
|
||||
* Calendar Export Service
|
||||
*/
|
||||
class ExportService {
|
||||
|
||||
public const FORMATS = ['ical', 'jcal', 'xcal'];
|
||||
private string $systemVersion;
|
||||
|
||||
public function __construct(ServerVersion $serverVersion) {
|
||||
$this->systemVersion = $serverVersion->getVersionString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates serialized content stream for a calendar and objects based in selected format
|
||||
*
|
||||
* @return Generator<string>
|
||||
*/
|
||||
public function export(ICalendarExport $calendar, CalendarExportOptions $options): Generator {
|
||||
// output start of serialized content based on selected format
|
||||
yield $this->exportStart($options->getFormat());
|
||||
// iterate through each returned vCalendar entry
|
||||
// extract each component except timezones, convert to appropriate format and output
|
||||
// extract any timezones and save them but do not output
|
||||
$timezones = [];
|
||||
foreach ($calendar->export($options) as $entry) {
|
||||
$consecutive = false;
|
||||
foreach ($entry->getComponents() as $vComponent) {
|
||||
if ($vComponent->name === 'VTIMEZONE') {
|
||||
if (isset($vComponent->TZID) && !isset($timezones[$vComponent->TZID->getValue()])) {
|
||||
$timezones[$vComponent->TZID->getValue()] = clone $vComponent;
|
||||
}
|
||||
} else {
|
||||
yield $this->exportObject($vComponent, $options->getFormat(), $consecutive);
|
||||
$consecutive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// iterate through each saved vTimezone entry, convert to appropriate format and output
|
||||
foreach ($timezones as $vComponent) {
|
||||
yield $this->exportObject($vComponent, $options->getFormat(), $consecutive);
|
||||
$consecutive = true;
|
||||
}
|
||||
// output end of serialized content based on selected format
|
||||
yield $this->exportFinish($options->getFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates serialized content start based on selected format
|
||||
*/
|
||||
private function exportStart(string $format): string {
|
||||
return match ($format) {
|
||||
'jcal' => '["vcalendar",[["version",{},"text","2.0"],["prodid",{},"text","-\/\/IDN nextcloud.com\/\/Calendar Export v' . $this->systemVersion . '\/\/EN"]],[',
|
||||
'xcal' => '<?xml version="1.0" encoding="UTF-8"?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><version><text>2.0</text></version><prodid><text>-//IDN nextcloud.com//Calendar Export v' . $this->systemVersion . '//EN</text></prodid></properties><components>',
|
||||
default => "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//IDN nextcloud.com//Calendar Export v" . $this->systemVersion . "//EN\n"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates serialized content end based on selected format
|
||||
*/
|
||||
private function exportFinish(string $format): string {
|
||||
return match ($format) {
|
||||
'jcal' => ']]',
|
||||
'xcal' => '</components></vcalendar></icalendar>',
|
||||
default => "END:VCALENDAR\n"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates serialized content for a component based on selected format
|
||||
*/
|
||||
private function exportObject(Component $vobject, string $format, bool $consecutive): string {
|
||||
return match ($format) {
|
||||
'jcal' => $consecutive ? ',' . Writer::writeJson($vobject) : Writer::writeJson($vobject),
|
||||
'xcal' => $this->exportObjectXml($vobject),
|
||||
default => Writer::write($vobject)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates serialized content for a component in xml format
|
||||
*/
|
||||
private function exportObjectXml(Component $vobject): string {
|
||||
$writer = new \Sabre\Xml\Writer();
|
||||
$writer->openMemory();
|
||||
$writer->setIndent(false);
|
||||
$vobject->xmlSerialize($writer);
|
||||
return $writer->outputMemory();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,12 +25,6 @@ use function array_values;
|
||||
|
||||
abstract class AbstractPrincipalBackend implements BackendInterface {
|
||||
|
||||
/** @var ProxyMapper */
|
||||
private $proxyMapper;
|
||||
|
||||
/** @var string */
|
||||
private $principalPrefix;
|
||||
|
||||
/** @var string */
|
||||
private $dbTableName;
|
||||
|
||||
@@ -45,13 +39,11 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
|
||||
private IUserSession $userSession,
|
||||
private IGroupManager $groupManager,
|
||||
private LoggerInterface $logger,
|
||||
ProxyMapper $proxyMapper,
|
||||
string $principalPrefix,
|
||||
private ProxyMapper $proxyMapper,
|
||||
private string $principalPrefix,
|
||||
string $dbPrefix,
|
||||
private string $cuType,
|
||||
) {
|
||||
$this->proxyMapper = $proxyMapper;
|
||||
$this->principalPrefix = $principalPrefix;
|
||||
$this->dbTableName = 'calendar_' . $dbPrefix . 's';
|
||||
$this->dbMetaDataTableName = $this->dbTableName . '_md';
|
||||
$this->dbForeignKeyName = $dbPrefix . '_id';
|
||||
|
||||
@@ -17,12 +17,13 @@ class Capabilities implements ICapability {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{dav: array{chunking: string, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}}
|
||||
* @return array{dav: array{chunking: string, public_shares_chunking: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}}
|
||||
*/
|
||||
public function getCapabilities() {
|
||||
$capabilities = [
|
||||
'dav' => [
|
||||
'chunking' => '1.0',
|
||||
'public_shares_chunking' => true,
|
||||
]
|
||||
];
|
||||
if ($this->config->getSystemValueBool('bulkupload.enabled', true)) {
|
||||
|
||||
@@ -137,7 +137,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
|
||||
$subSelect->select('id')
|
||||
->from('dav_shares', 'd')
|
||||
->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(\OCA\DAV\CardDAV\Sharing\Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
|
||||
->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY));
|
||||
->andWhere($subSelect->expr()->eq('d.principaluri', $select->createNamedParameter($principalUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
|
||||
|
||||
|
||||
$select->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access'])
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Command;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Sharing\Backend;
|
||||
use OCA\DAV\CalDAV\Sharing\Service;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\Sharing\Backend as BackendAlias;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'dav:clear-calendar-unshares',
|
||||
description: 'Clear calendar unshares for a user',
|
||||
hidden: false,
|
||||
)]
|
||||
class ClearCalendarUnshares extends Command {
|
||||
public function __construct(
|
||||
private IUserManager $userManager,
|
||||
private IAppConfig $appConfig,
|
||||
private Principal $principal,
|
||||
private CalDavBackend $caldav,
|
||||
private Backend $sharingBackend,
|
||||
private Service $sharingService,
|
||||
private SharingMapper $mapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
$this->addArgument(
|
||||
'uid',
|
||||
InputArgument::REQUIRED,
|
||||
'User whose unshares to clear'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$user = (string)$input->getArgument('uid');
|
||||
if (!$this->userManager->userExists($user)) {
|
||||
throw new \InvalidArgumentException("User $user is unknown");
|
||||
}
|
||||
|
||||
$principal = $this->principal->getPrincipalByPath('principals/users/' . $user);
|
||||
if ($principal === null) {
|
||||
throw new \InvalidArgumentException("Unable to fetch principal for user $user ");
|
||||
}
|
||||
|
||||
$shares = $this->mapper->getSharesByPrincipals([$principal['uri']], 'calendar');
|
||||
$unshares = array_filter($shares, static fn ($share) => $share['access'] === BackendAlias::ACCESS_UNSHARED);
|
||||
|
||||
if (count($unshares) === 0) {
|
||||
$output->writeln("User $user has no calendar unshares");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$rows = array_map(fn ($share) => $this->formatCalendarUnshare($share), $shares);
|
||||
|
||||
$table = new Table($output);
|
||||
$table
|
||||
->setHeaders(['Share Id', 'Calendar Id', 'Calendar URI', 'Calendar Name'])
|
||||
->setRows($rows)
|
||||
->render();
|
||||
|
||||
$output->writeln('');
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new ConfirmationQuestion('Please confirm to delete the above calendar unshare entries [y/n]', false);
|
||||
|
||||
if ($helper->ask($input, $output, $question)) {
|
||||
$this->mapper->deleteUnsharesByPrincipal($principal['uri'], 'calendar');
|
||||
$output->writeln("Calendar unshares for user $user deleted");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function formatCalendarUnshare(array $share): array {
|
||||
$calendarInfo = $this->caldav->getCalendarById($share['resourceid']);
|
||||
|
||||
$resourceUri = 'Resource not found';
|
||||
$resourceName = '';
|
||||
|
||||
if ($calendarInfo !== null) {
|
||||
$resourceUri = $calendarInfo['uri'];
|
||||
$resourceName = $calendarInfo['{DAV:}displayname'];
|
||||
}
|
||||
|
||||
return [
|
||||
$share['id'],
|
||||
$share['resourceid'],
|
||||
$resourceUri,
|
||||
$resourceName,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\DAV\Command;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCA\DAV\CalDAV\Export\ExportService;
|
||||
use OCP\Calendar\CalendarExportOptions;
|
||||
use OCP\Calendar\ICalendarExport;
|
||||
use OCP\Calendar\IManager;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Calendar Export Command
|
||||
*
|
||||
* Used to export data from supported calendars to disk or stdout
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'calendar:export',
|
||||
description: 'Export calendar data from supported calendars to disk or stdout',
|
||||
hidden: false
|
||||
)]
|
||||
class ExportCalendar extends Command {
|
||||
public function __construct(
|
||||
private IUserManager $userManager,
|
||||
private IManager $calendarManager,
|
||||
private ExportService $exportService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
$this->setName('calendar:export')
|
||||
->setDescription('Export calendar data from supported calendars to disk or stdout')
|
||||
->addArgument('uid', InputArgument::REQUIRED, 'Id of system user')
|
||||
->addArgument('uri', InputArgument::REQUIRED, 'Uri of calendar')
|
||||
->addOption('format', null, InputOption::VALUE_REQUIRED, 'Format of output (ical, jcal, xcal) defaults to ical', 'ical')
|
||||
->addOption('location', null, InputOption::VALUE_REQUIRED, 'Location of where to write the output. defaults to stdout');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$userId = $input->getArgument('uid');
|
||||
$calendarId = $input->getArgument('uri');
|
||||
$format = $input->getOption('format');
|
||||
$location = $input->getOption('location');
|
||||
|
||||
if (!$this->userManager->userExists($userId)) {
|
||||
throw new InvalidArgumentException("User <$userId> not found.");
|
||||
}
|
||||
// retrieve calendar and evaluate if export is supported
|
||||
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]);
|
||||
if ($calendars === []) {
|
||||
throw new InvalidArgumentException("Calendar <$calendarId> not found.");
|
||||
}
|
||||
$calendar = $calendars[0];
|
||||
if (!$calendar instanceof ICalendarExport) {
|
||||
throw new InvalidArgumentException("Calendar <$calendarId> does not support exporting");
|
||||
}
|
||||
// construct options object
|
||||
$options = new CalendarExportOptions();
|
||||
// evaluate if provided format is supported
|
||||
if (!in_array($format, ExportService::FORMATS, true)) {
|
||||
throw new InvalidArgumentException("Format <$format> is not valid.");
|
||||
}
|
||||
$options->setFormat($format);
|
||||
// evaluate is a valid location was given and is usable otherwise output to stdout
|
||||
if ($location !== null) {
|
||||
$handle = fopen($location, 'wb');
|
||||
if ($handle === false) {
|
||||
throw new InvalidArgumentException("Location <$location> is not valid. Can not open location for write operation.");
|
||||
}
|
||||
|
||||
foreach ($this->exportService->export($calendar, $options) as $chunk) {
|
||||
fwrite($handle, $chunk);
|
||||
}
|
||||
fclose($handle);
|
||||
} else {
|
||||
foreach ($this->exportService->export($calendar, $options) as $chunk) {
|
||||
$output->writeln($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Command;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Sharing\Backend;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'dav:list-calendar-shares',
|
||||
description: 'List all calendar shares for a user',
|
||||
hidden: false,
|
||||
)]
|
||||
class ListCalendarShares extends Command {
|
||||
public function __construct(
|
||||
private IUserManager $userManager,
|
||||
private Principal $principal,
|
||||
private CalDavBackend $caldav,
|
||||
private SharingMapper $mapper,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
$this->addArgument(
|
||||
'uid',
|
||||
InputArgument::REQUIRED,
|
||||
'User whose calendar shares will be listed'
|
||||
);
|
||||
$this->addOption(
|
||||
'calendar-id',
|
||||
'',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'List only shares for the given calendar id id',
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$user = (string)$input->getArgument('uid');
|
||||
if (!$this->userManager->userExists($user)) {
|
||||
throw new \InvalidArgumentException("User $user is unknown");
|
||||
}
|
||||
|
||||
$principal = $this->principal->getPrincipalByPath('principals/users/' . $user);
|
||||
if ($principal === null) {
|
||||
throw new \InvalidArgumentException("Unable to fetch principal for user $user");
|
||||
}
|
||||
|
||||
$memberships = array_merge(
|
||||
[$principal['uri']],
|
||||
$this->principal->getGroupMembership($principal['uri']),
|
||||
$this->principal->getCircleMembership($principal['uri']),
|
||||
);
|
||||
|
||||
$shares = $this->mapper->getSharesByPrincipals($memberships, 'calendar');
|
||||
|
||||
$calendarId = $input->getOption('calendar-id');
|
||||
if ($calendarId !== null) {
|
||||
$shares = array_filter($shares, fn ($share) => $share['resourceid'] === (int)$calendarId);
|
||||
}
|
||||
|
||||
$rows = array_map(fn ($share) => $this->formatCalendarShare($share), $shares);
|
||||
|
||||
if (count($rows) > 0) {
|
||||
$table = new Table($output);
|
||||
$table
|
||||
->setHeaders(['Share Id', 'Calendar Id', 'Calendar URI', 'Calendar Name', 'Calendar Owner', 'Access By', 'Permissions'])
|
||||
->setRows($rows)
|
||||
->render();
|
||||
} else {
|
||||
$output->writeln("User $user has no calendar shares");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function formatCalendarShare(array $share): array {
|
||||
$calendarInfo = $this->caldav->getCalendarById($share['resourceid']);
|
||||
|
||||
$calendarUri = 'Resource not found';
|
||||
$calendarName = '';
|
||||
$calendarOwner = '';
|
||||
|
||||
if ($calendarInfo !== null) {
|
||||
$calendarUri = $calendarInfo['uri'];
|
||||
$calendarName = $calendarInfo['{DAV:}displayname'];
|
||||
$calendarOwner = $calendarInfo['{http://nextcloud.com/ns}owner-displayname'] . ' (' . $calendarInfo['principaluri'] . ')';
|
||||
}
|
||||
|
||||
$accessBy = match (true) {
|
||||
str_starts_with($share['principaluri'], 'principals/users/') => 'Individual',
|
||||
str_starts_with($share['principaluri'], 'principals/groups/') => 'Group (' . $share['principaluri'] . ')',
|
||||
str_starts_with($share['principaluri'], 'principals/circles/') => 'Team (' . $share['principaluri'] . ')',
|
||||
default => $share['principaluri'],
|
||||
};
|
||||
|
||||
$permissions = match ($share['access']) {
|
||||
Backend::ACCESS_READ => 'Read',
|
||||
Backend::ACCESS_READ_WRITE => 'Read/Write',
|
||||
Backend::ACCESS_UNSHARED => 'Unshare',
|
||||
default => $share['access'],
|
||||
};
|
||||
|
||||
return [
|
||||
$share['id'],
|
||||
$share['resourceid'],
|
||||
$calendarUri,
|
||||
$calendarName,
|
||||
$calendarOwner,
|
||||
$accessBy,
|
||||
$permissions,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -26,20 +26,6 @@ class ChecksumUpdatePlugin extends ServerPlugin {
|
||||
return 'checksumupdate';
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getHTTPMethods($path): array {
|
||||
$tree = $this->server->tree;
|
||||
|
||||
if ($tree->nodeExists($path)) {
|
||||
$node = $tree->getNodeForPath($path);
|
||||
if ($node instanceof File) {
|
||||
return ['PATCH'];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getFeatures(): array {
|
||||
return ['nextcloud-checksum-update'];
|
||||
|
||||
@@ -13,7 +13,9 @@ use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
|
||||
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
|
||||
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
|
||||
use OCA\DAV\Storage\PublicShareWrapper;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Constants;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\ForbiddenException;
|
||||
@@ -172,7 +174,20 @@ class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuot
|
||||
* @throws \Sabre\DAV\Exception\ServiceUnavailable
|
||||
*/
|
||||
public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
|
||||
if (!$this->info->isReadable()) {
|
||||
$storage = $this->info->getStorage();
|
||||
$allowDirectory = false;
|
||||
|
||||
// Checking if we're in a file drop
|
||||
// If we are, then only PUT and MKCOL are allowed (see plugin)
|
||||
// so we are safe to return the directory without a risk of
|
||||
// leaking files and folders structure.
|
||||
if ($storage instanceof PublicShareWrapper) {
|
||||
$share = $storage->getShare();
|
||||
$allowDirectory = ($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ;
|
||||
}
|
||||
|
||||
// For file drop we need to be allowed to read the directory with the nickname
|
||||
if (!$allowDirectory && !$this->info->isReadable()) {
|
||||
// avoid detecting files through this way
|
||||
throw new NotFound();
|
||||
}
|
||||
@@ -198,6 +213,11 @@ class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuot
|
||||
if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
|
||||
$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
|
||||
} else {
|
||||
// In case reading a directory was allowed but it turns out the node was a not a directory, reject it now.
|
||||
if (!$this->info->isReadable()) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$node = new File($this->fileView, $info, $this->shareManager, $request, $l10n);
|
||||
}
|
||||
if ($this->tree) {
|
||||
|
||||
@@ -11,6 +11,7 @@ use OC\AppFramework\Http\Request;
|
||||
use OC\FilesMetadata\Model\FilesMetadata;
|
||||
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
|
||||
use OCA\Files_Sharing\External\Mount as SharingExternalMount;
|
||||
use OCP\Accounts\IAccountManager;
|
||||
use OCP\Constants;
|
||||
use OCP\Files\ForbiddenException;
|
||||
use OCP\Files\IFilenameValidator;
|
||||
@@ -91,6 +92,7 @@ class FilesPlugin extends ServerPlugin {
|
||||
private IPreview $previewManager,
|
||||
private IUserSession $userSession,
|
||||
private IFilenameValidator $validator,
|
||||
private IAccountManager $accountManager,
|
||||
private bool $isPublic = false,
|
||||
private bool $downloadAttachment = true,
|
||||
) {
|
||||
@@ -361,9 +363,26 @@ class FilesPlugin extends ServerPlugin {
|
||||
$owner = $node->getOwner();
|
||||
if (!$owner) {
|
||||
return null;
|
||||
} else {
|
||||
}
|
||||
|
||||
// Get current user to see if we're in a public share or not
|
||||
$user = $this->userSession->getUser();
|
||||
|
||||
// If the user is logged in, we can return the display name
|
||||
if ($user !== null) {
|
||||
return $owner->getDisplayName();
|
||||
}
|
||||
|
||||
// Check if the user published their display name
|
||||
$ownerAccount = $this->accountManager->getAccount($owner);
|
||||
$ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
|
||||
|
||||
// Since we are not logged in, we need to have at least the published scope
|
||||
if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) {
|
||||
return $owner->getDisplayName();
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
|
||||
@@ -701,15 +720,15 @@ class FilesPlugin extends ServerPlugin {
|
||||
*/
|
||||
public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
|
||||
// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
|
||||
if (!$this->server->tree->nodeExists($filePath)) {
|
||||
return;
|
||||
}
|
||||
$node = $this->server->tree->getNodeForPath($filePath);
|
||||
if ($node instanceof Node) {
|
||||
$fileId = $node->getFileId();
|
||||
if (!is_null($fileId)) {
|
||||
$this->server->httpResponse->setHeader('OC-FileId', $fileId);
|
||||
try {
|
||||
$node = $this->server->tree->getNodeForPath($filePath);
|
||||
if ($node instanceof Node) {
|
||||
$fileId = $node->getFileId();
|
||||
if (!is_null($fileId)) {
|
||||
$this->server->httpResponse->setHeader('OC-FileId', $fileId);
|
||||
}
|
||||
}
|
||||
} catch (NotFound) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,6 @@ class Principal implements BackendInterface {
|
||||
/** @var bool */
|
||||
private $hasCircles;
|
||||
|
||||
/** @var ProxyMapper */
|
||||
private $proxyMapper;
|
||||
|
||||
/** @var KnownUserService */
|
||||
private $knownUserService;
|
||||
|
||||
@@ -54,7 +51,7 @@ class Principal implements BackendInterface {
|
||||
private IShareManager $shareManager,
|
||||
private IUserSession $userSession,
|
||||
private IAppManager $appManager,
|
||||
ProxyMapper $proxyMapper,
|
||||
private ProxyMapper $proxyMapper,
|
||||
KnownUserService $knownUserService,
|
||||
private IConfig $config,
|
||||
private IFactory $languageFactory,
|
||||
@@ -62,7 +59,6 @@ class Principal implements BackendInterface {
|
||||
) {
|
||||
$this->principalPrefix = trim($principalPrefix, '/');
|
||||
$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
|
||||
$this->proxyMapper = $proxyMapper;
|
||||
$this->knownUserService = $knownUserService;
|
||||
}
|
||||
|
||||
@@ -155,6 +151,11 @@ class Principal implements BackendInterface {
|
||||
'uri' => 'principals/system/' . $name,
|
||||
'{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
|
||||
];
|
||||
} elseif ($prefix === 'principals/shares') {
|
||||
return [
|
||||
'uri' => 'principals/shares/' . $name,
|
||||
'{DAV:}displayname' => $name,
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use OCP\Defaults;
|
||||
use OCP\IRequest;
|
||||
use OCP\ISession;
|
||||
use OCP\Security\Bruteforce\IThrottler;
|
||||
use OCP\Security\Bruteforce\MaxDelayReached;
|
||||
use OCP\Share\Exceptions\ShareNotFound;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\Share\IShare;
|
||||
@@ -56,6 +57,7 @@ class PublicAuth extends AbstractBasic {
|
||||
*
|
||||
* @return array
|
||||
* @throws NotAuthenticated
|
||||
* @throws MaxDelayReached
|
||||
* @throws ServiceUnavailable
|
||||
*/
|
||||
public function check(RequestInterface $request, ResponseInterface $response): array {
|
||||
@@ -75,7 +77,8 @@ class PublicAuth extends AbstractBasic {
|
||||
}
|
||||
|
||||
return $this->checkToken();
|
||||
} catch (NotAuthenticated $e) {
|
||||
} catch (NotAuthenticated|MaxDelayReached $e) {
|
||||
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$class = get_class($e);
|
||||
@@ -94,7 +97,7 @@ class PublicAuth extends AbstractBasic {
|
||||
$path = $this->request->getPathInfo() ?: '';
|
||||
// ['', 'dav', 'files', 'token']
|
||||
$splittedPath = explode('/', $path);
|
||||
|
||||
|
||||
if (count($splittedPath) < 4 || $splittedPath[3] === '') {
|
||||
throw new NotFound();
|
||||
}
|
||||
@@ -176,7 +179,7 @@ class PublicAuth extends AbstractBasic {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if ($this->session->exists(PublicAuth::DAV_AUTHENTICATED)
|
||||
&& $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) {
|
||||
return true;
|
||||
|
||||
@@ -8,17 +8,23 @@
|
||||
namespace OCA\DAV\Connector\Sabre;
|
||||
|
||||
use OC\Files\View;
|
||||
use OC\KnownUser\KnownUserService;
|
||||
use OCA\DAV\AppInfo\PluginManager;
|
||||
use OCA\DAV\CalDAV\DefaultCalendarValidator;
|
||||
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
|
||||
use OCA\DAV\DAV\CustomPropertiesBackend;
|
||||
use OCA\DAV\DAV\ViewOnlyPlugin;
|
||||
use OCA\DAV\Files\BrowserErrorPagePlugin;
|
||||
use OCA\DAV\Files\Sharing\RootCollection;
|
||||
use OCA\DAV\Upload\CleanupService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\Accounts\IAccountManager;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IFilenameValidator;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Mount\IMountManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
@@ -27,12 +33,14 @@ use OCP\IL10N;
|
||||
use OCP\IPreview;
|
||||
use OCP\IRequest;
|
||||
use OCP\ITagManager;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IUserSession;
|
||||
use OCP\SabrePluginEvent;
|
||||
use OCP\SystemTag\ISystemTagManager;
|
||||
use OCP\SystemTag\ISystemTagObjectMapper;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Auth\Plugin;
|
||||
use Sabre\DAV\SimpleCollection;
|
||||
|
||||
class ServerFactory {
|
||||
|
||||
@@ -53,13 +61,22 @@ class ServerFactory {
|
||||
/**
|
||||
* @param callable $viewCallBack callback that should return the view for the dav endpoint
|
||||
*/
|
||||
public function createServer(string $baseUri,
|
||||
public function createServer(
|
||||
bool $isPublicShare,
|
||||
string $baseUri,
|
||||
string $requestUri,
|
||||
Plugin $authPlugin,
|
||||
callable $viewCallBack): Server {
|
||||
callable $viewCallBack,
|
||||
): Server {
|
||||
// Fire up server
|
||||
$objectTree = new ObjectTree();
|
||||
$server = new Server($objectTree);
|
||||
if ($isPublicShare) {
|
||||
$rootCollection = new SimpleCollection('root');
|
||||
$tree = new CachingTree($rootCollection);
|
||||
} else {
|
||||
$rootCollection = null;
|
||||
$tree = new ObjectTree();
|
||||
}
|
||||
$server = new Server($tree);
|
||||
// Set URL explicitly due to reverse-proxy situations
|
||||
$server->httpRequest->setUrl($requestUri);
|
||||
$server->setBaseUri($baseUri);
|
||||
@@ -80,7 +97,7 @@ class ServerFactory {
|
||||
$server->addPlugin(new RequestIdHeaderPlugin($this->request));
|
||||
|
||||
$server->addPlugin(new ZipFolderPlugin(
|
||||
$objectTree,
|
||||
$tree,
|
||||
$this->logger,
|
||||
$this->eventDispatcher,
|
||||
));
|
||||
@@ -100,7 +117,7 @@ class ServerFactory {
|
||||
}
|
||||
|
||||
// wait with registering these until auth is handled and the filesystem is setup
|
||||
$server->on('beforeMethod:*', function () use ($server, $objectTree, $viewCallBack): void {
|
||||
$server->on('beforeMethod:*', function () use ($server, $tree, $viewCallBack, $isPublicShare, $rootCollection): void {
|
||||
// ensure the skeleton is copied
|
||||
$userFolder = \OC::$server->getUserFolder();
|
||||
|
||||
@@ -114,20 +131,55 @@ class ServerFactory {
|
||||
|
||||
// Create Nextcloud Dir
|
||||
if ($rootInfo->getType() === 'dir') {
|
||||
$root = new Directory($view, $rootInfo, $objectTree);
|
||||
$root = new Directory($view, $rootInfo, $tree);
|
||||
} else {
|
||||
$root = new File($view, $rootInfo);
|
||||
}
|
||||
$objectTree->init($root, $view, $this->mountManager);
|
||||
|
||||
if ($isPublicShare) {
|
||||
$userPrincipalBackend = new Principal(
|
||||
\OCP\Server::get(IUserManager::class),
|
||||
\OCP\Server::get(IGroupManager::class),
|
||||
\OCP\Server::get(IAccountManager::class),
|
||||
\OCP\Server::get(\OCP\Share\IManager::class),
|
||||
\OCP\Server::get(IUserSession::class),
|
||||
\OCP\Server::get(IAppManager::class),
|
||||
\OCP\Server::get(ProxyMapper::class),
|
||||
\OCP\Server::get(KnownUserService::class),
|
||||
\OCP\Server::get(IConfig::class),
|
||||
\OC::$server->getL10NFactory(),
|
||||
);
|
||||
|
||||
// Mount the share collection at /public.php/dav/shares/<share token>
|
||||
$rootCollection->addChild(new RootCollection(
|
||||
$root,
|
||||
$userPrincipalBackend,
|
||||
'principals/shares',
|
||||
));
|
||||
|
||||
// Mount the upload collection at /public.php/dav/uploads/<share token>
|
||||
$rootCollection->addChild(new \OCA\DAV\Upload\RootCollection(
|
||||
$userPrincipalBackend,
|
||||
'principals/shares',
|
||||
\OCP\Server::get(CleanupService::class),
|
||||
\OCP\Server::get(IRootFolder::class),
|
||||
\OCP\Server::get(IUserSession::class),
|
||||
\OCP\Server::get(\OCP\Share\IManager::class),
|
||||
));
|
||||
} else {
|
||||
/** @var ObjectTree $tree */
|
||||
$tree->init($root, $view, $this->mountManager);
|
||||
}
|
||||
|
||||
$server->addPlugin(
|
||||
new FilesPlugin(
|
||||
$objectTree,
|
||||
$tree,
|
||||
$this->config,
|
||||
$this->request,
|
||||
$this->previewManager,
|
||||
$this->userSession,
|
||||
\OCP\Server::get(IFilenameValidator::class),
|
||||
\OCP\Server::get(IAccountManager::class),
|
||||
false,
|
||||
!$this->config->getSystemValue('debug', false)
|
||||
)
|
||||
@@ -141,16 +193,16 @@ class ServerFactory {
|
||||
));
|
||||
|
||||
if ($this->userSession->isLoggedIn()) {
|
||||
$server->addPlugin(new TagsPlugin($objectTree, $this->tagManager, $this->eventDispatcher, $this->userSession));
|
||||
$server->addPlugin(new TagsPlugin($tree, $this->tagManager, $this->eventDispatcher, $this->userSession));
|
||||
$server->addPlugin(new SharesPlugin(
|
||||
$objectTree,
|
||||
$tree,
|
||||
$this->userSession,
|
||||
$userFolder,
|
||||
\OCP\Server::get(\OCP\Share\IManager::class)
|
||||
));
|
||||
$server->addPlugin(new CommentPropertiesPlugin(\OCP\Server::get(ICommentsManager::class), $this->userSession));
|
||||
$server->addPlugin(new FilesReportPlugin(
|
||||
$objectTree,
|
||||
$tree,
|
||||
$view,
|
||||
\OCP\Server::get(ISystemTagManager::class),
|
||||
\OCP\Server::get(ISystemTagObjectMapper::class),
|
||||
@@ -165,7 +217,7 @@ class ServerFactory {
|
||||
new \Sabre\DAV\PropertyStorage\Plugin(
|
||||
new CustomPropertiesBackend(
|
||||
$server,
|
||||
$objectTree,
|
||||
$tree,
|
||||
$this->databaseConnection,
|
||||
$this->userSession->getUser(),
|
||||
\OCP\Server::get(DefaultCalendarValidator::class),
|
||||
|
||||
@@ -90,14 +90,6 @@ abstract class Backend {
|
||||
|
||||
// Delete any possible direct shares (since the frontend does not separate between them)
|
||||
$this->service->deleteShare($shareable->getResourceId(), $principal);
|
||||
|
||||
// Check if a user has a groupshare that they're trying to free themselves from
|
||||
// If so we need to add a self::ACCESS_UNSHARED row
|
||||
if (!str_contains($principal, 'group')
|
||||
&& $this->service->hasGroupShare($oldShares)
|
||||
) {
|
||||
$this->service->unshare($shareable->getResourceId(), $principal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,4 +196,45 @@ abstract class Backend {
|
||||
}
|
||||
return $acl;
|
||||
}
|
||||
|
||||
public function unshare(IShareable $shareable, string $principalUri): bool {
|
||||
$this->shareCache->clear();
|
||||
|
||||
$principal = $this->principalBackend->findByUri($principalUri, '');
|
||||
if (empty($principal)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($shareable->getOwner() === $principal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete any possible direct shares (since the frontend does not separate between them)
|
||||
$this->service->deleteShare($shareable->getResourceId(), $principal);
|
||||
|
||||
$needsUnshare = $this->hasAccessByGroupOrCirclesMembership(
|
||||
$shareable->getResourceId(),
|
||||
$principal
|
||||
);
|
||||
|
||||
if ($needsUnshare) {
|
||||
$this->service->unshare($shareable->getResourceId(), $principal);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function hasAccessByGroupOrCirclesMembership(int $resourceId, string $principal) {
|
||||
$memberships = array_merge(
|
||||
$this->principalBackend->getGroupMembership($principal, true),
|
||||
$this->principalBackend->getCircleMembership($principal)
|
||||
);
|
||||
|
||||
$shares = array_column(
|
||||
$this->service->getShares($resourceId),
|
||||
'principaluri'
|
||||
);
|
||||
|
||||
return count(array_intersect($memberships, $shares)) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,4 +110,28 @@ class SharingMapper {
|
||||
->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function getSharesByPrincipals(array $principals, string $resourceType): array {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$result = $query->select(['id', 'principaluri', 'type', 'access', 'resourceid'])
|
||||
->from('dav_shares')
|
||||
->where($query->expr()->in('principaluri', $query->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
|
||||
->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
|
||||
->orderBy('id')
|
||||
->executeQuery();
|
||||
|
||||
$rows = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function deleteUnsharesByPrincipal(string $principal, string $resourceType): void {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->delete('dav_shares')
|
||||
->where($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
|
||||
->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
|
||||
->andWhere($query->expr()->eq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)))
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,14 +50,4 @@ abstract class SharingService {
|
||||
public function getSharesForIds(array $resourceIds): array {
|
||||
return $this->mapper->getSharesForIds($resourceIds, $this->getResourceType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $oldShares
|
||||
* @return bool
|
||||
*/
|
||||
public function hasGroupShare(array $oldShares): bool {
|
||||
return !empty(array_filter($oldShares, function (array $share) {
|
||||
return $share['{http://owncloud.org/ns}group-share'] === true;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,18 @@ class PropertyMapper extends QBMapper {
|
||||
return $this->findEntities($selectQb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Property[]
|
||||
*/
|
||||
public function findPropertiesByPath(string $userId, string $path): array {
|
||||
$selectQb = $this->db->getQueryBuilder();
|
||||
$selectQb->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$selectQb->expr()->eq('userid', $selectQb->createNamedParameter($userId)),
|
||||
$selectQb->expr()->eq('propertypath', $selectQb->createNamedParameter($path)),
|
||||
);
|
||||
return $this->findEntities($selectQb);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class DirectHome implements ICollection {
|
||||
} catch (DoesNotExistException $e) {
|
||||
// Since the token space is so huge only throttle on non-existing token
|
||||
$this->throttler->registerAttempt('directlink', $this->request->getRemoteAddress());
|
||||
$this->throttler->sleepDelay($this->request->getRemoteAddress(), 'directlink');
|
||||
$this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), 'directlink');
|
||||
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
@@ -8,22 +8,24 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace OCA\DAV\Events;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
|
||||
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
|
||||
|
||||
/**
|
||||
* Class CalendarShareUpdatedEvent
|
||||
*
|
||||
* @package OCA\DAV\Events
|
||||
* @since 20.0.0
|
||||
*
|
||||
* @psalm-import-type CalendarInfo from CalDavBackend
|
||||
*/
|
||||
class CalendarShareUpdatedEvent extends Event {
|
||||
/**
|
||||
* CalendarShareUpdatedEvent constructor.
|
||||
*
|
||||
* @param int $calendarId
|
||||
* @param array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string } $calendarData
|
||||
* @psalm-param CalendarInfo $calendarData
|
||||
* @param array $calendarData
|
||||
* @param list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> $oldShares
|
||||
* @param list<array{href: string, commonName: string, readOnly: bool}> $added
|
||||
* @param list<string> $removed
|
||||
@@ -47,7 +49,8 @@ class CalendarShareUpdatedEvent extends Event {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }
|
||||
* @psalm-return CalendarInfo
|
||||
* @return array
|
||||
* @since 20.0.0
|
||||
*/
|
||||
public function getCalendarData(): array {
|
||||
|
||||
@@ -11,6 +11,7 @@ use OC\AppFramework\Http\Request;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\IRequest;
|
||||
use OCP\Security\Bruteforce\MaxDelayReached;
|
||||
use OCP\Template\ITemplateManager;
|
||||
use Sabre\DAV\Exception;
|
||||
use Sabre\DAV\Server;
|
||||
@@ -60,6 +61,9 @@ class BrowserErrorPagePlugin extends ServerPlugin {
|
||||
if ($ex instanceof Exception) {
|
||||
$httpCode = $ex->getHTTPCode();
|
||||
$headers = $ex->getHTTPHeaders($this->server);
|
||||
} elseif ($ex instanceof MaxDelayReached) {
|
||||
$httpCode = 429;
|
||||
$headers = [];
|
||||
} else {
|
||||
$httpCode = 500;
|
||||
$headers = [];
|
||||
@@ -81,7 +85,7 @@ class BrowserErrorPagePlugin extends ServerPlugin {
|
||||
$request = \OCP\Server::get(IRequest::class);
|
||||
|
||||
$templateName = 'exception';
|
||||
if ($httpCode === 403 || $httpCode === 404) {
|
||||
if ($httpCode === 403 || $httpCode === 404 || $httpCode === 429) {
|
||||
$templateName = (string)$httpCode;
|
||||
}
|
||||
|
||||
|
||||
@@ -422,10 +422,16 @@ class FileSearchBackend implements ISearchBackend {
|
||||
$field = $this->mapPropertyNameToColumn($property);
|
||||
}
|
||||
|
||||
try {
|
||||
$castedValue = $this->castValue($property, $value ?? '');
|
||||
} catch (\Error $e) {
|
||||
throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e);
|
||||
}
|
||||
|
||||
return new SearchComparison(
|
||||
$trimmedType,
|
||||
$field,
|
||||
$this->castValue($property, $value ?? ''),
|
||||
$castedValue,
|
||||
$extra ?? ''
|
||||
);
|
||||
|
||||
|
||||
@@ -36,57 +36,136 @@ class FilesDropPlugin extends ServerPlugin {
|
||||
|
||||
/**
|
||||
* This initializes the plugin.
|
||||
*
|
||||
* @param \Sabre\DAV\Server $server Sabre server
|
||||
*
|
||||
* @return void
|
||||
* @throws MethodNotAllowed
|
||||
* It is ONLY initialized by the server on a file drop request.
|
||||
*/
|
||||
public function initialize(\Sabre\DAV\Server $server): void {
|
||||
$server->on('beforeMethod:*', [$this, 'beforeMethod'], 999);
|
||||
$server->on('method:MKCOL', [$this, 'onMkcol']);
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
public function beforeMethod(RequestInterface $request, ResponseInterface $response): void {
|
||||
public function onMkcol(RequestInterface $request, ResponseInterface $response) {
|
||||
if (!$this->enabled || $this->share === null || $this->view === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow file drop
|
||||
if ($request->getMethod() !== 'PUT') {
|
||||
throw new MethodNotAllowed('Only PUT is allowed on files drop');
|
||||
// If this is a folder creation request we need
|
||||
// to fake a success so we can pretend every
|
||||
// folder now exists.
|
||||
$response->setStatus(201);
|
||||
return false;
|
||||
}
|
||||
|
||||
public function beforeMethod(RequestInterface $request, ResponseInterface $response) {
|
||||
if (!$this->enabled || $this->share === null || $this->view === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always upload at the root level
|
||||
$path = explode('/', $request->getPath());
|
||||
$path = array_pop($path);
|
||||
// Retrieve the nickname from the request
|
||||
$nickname = $request->hasHeader('X-NC-Nickname')
|
||||
? trim(urldecode($request->getHeader('X-NC-Nickname')))
|
||||
: null;
|
||||
|
||||
//
|
||||
if ($request->getMethod() !== 'PUT') {
|
||||
// If uploading subfolders we need to ensure they get created
|
||||
// within the nickname folder
|
||||
if ($request->getMethod() === 'MKCOL') {
|
||||
if (!$nickname) {
|
||||
throw new MethodNotAllowed('A nickname header is required when uploading subfolders');
|
||||
}
|
||||
} else {
|
||||
throw new MethodNotAllowed('Only PUT is allowed on files drop');
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a folder creation request
|
||||
// let's stop there and let the onMkcol handle it
|
||||
if ($request->getMethod() === 'MKCOL') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Now if we create a file, we need to create the
|
||||
// full path along the way. We'll only handle conflict
|
||||
// resolution on file conflicts, but not on folders.
|
||||
|
||||
// e.g files/dCP8yn3N86EK9sL/Folder/image.jpg
|
||||
$path = $request->getPath();
|
||||
$token = $this->share->getToken();
|
||||
|
||||
// e.g files/dCP8yn3N86EK9sL
|
||||
$rootPath = substr($path, 0, strpos($path, $token) + strlen($token));
|
||||
// e.g /Folder/image.jpg
|
||||
$relativePath = substr($path, strlen($rootPath));
|
||||
$isRootUpload = substr_count($relativePath, '/') === 1;
|
||||
|
||||
// Extract the attributes for the file request
|
||||
$isFileRequest = false;
|
||||
$attributes = $this->share->getAttributes();
|
||||
$nickName = $request->hasHeader('X-NC-Nickname') ? urldecode($request->getHeader('X-NC-Nickname')) : null;
|
||||
if ($attributes !== null) {
|
||||
$isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true;
|
||||
}
|
||||
|
||||
// We need a valid nickname for file requests
|
||||
if ($isFileRequest && ($nickName == null || trim($nickName) === '')) {
|
||||
throw new MethodNotAllowed('Nickname is required for file requests');
|
||||
if ($isFileRequest && !$nickname) {
|
||||
throw new MethodNotAllowed('A nickname header is required for file requests');
|
||||
}
|
||||
|
||||
// If this is a file request we need to create a folder for the user
|
||||
if ($isFileRequest) {
|
||||
// Check if the folder already exists
|
||||
if (!($this->view->file_exists($nickName) === true)) {
|
||||
$this->view->mkdir($nickName);
|
||||
}
|
||||
|
||||
// We're only allowing the upload of
|
||||
// long path with subfolders if a nickname is set.
|
||||
// This prevents confusion when uploading files and help
|
||||
// classify them by uploaders.
|
||||
if (!$nickname && !$isRootUpload) {
|
||||
throw new MethodNotAllowed('A nickname header is required when uploading subfolders');
|
||||
}
|
||||
|
||||
// If we have a nickname, let's put everything inside
|
||||
if ($nickname) {
|
||||
// Put all files in the subfolder
|
||||
$path = $nickName . '/' . $path;
|
||||
$relativePath = '/' . $nickname . '/' . $relativePath;
|
||||
$relativePath = str_replace('//', '/', $relativePath);
|
||||
}
|
||||
|
||||
$newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view);
|
||||
$url = $request->getBaseUrl() . $newName;
|
||||
|
||||
// Create the folders along the way
|
||||
$folders = $this->getPathSegments(dirname($relativePath));
|
||||
foreach ($folders as $folder) {
|
||||
if ($folder === '') {
|
||||
continue;
|
||||
} // skip empty parts
|
||||
if (!$this->view->file_exists($folder)) {
|
||||
$this->view->mkdir($folder);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally handle conflicts on the end files
|
||||
$noConflictPath = \OC_Helper::buildNotExistingFileNameForView(dirname($relativePath), basename($relativePath), $this->view);
|
||||
$path = '/files/' . $token . '/' . $noConflictPath;
|
||||
$url = $request->getBaseUrl() . str_replace('//', '/', $path);
|
||||
$request->setUrl($url);
|
||||
}
|
||||
|
||||
private function getPathSegments(string $path): array {
|
||||
// Normalize slashes and remove trailing slash
|
||||
$path = rtrim(str_replace('\\', '/', $path), '/');
|
||||
|
||||
// Handle absolute paths starting with /
|
||||
$isAbsolute = str_starts_with($path, '/');
|
||||
|
||||
$segments = explode('/', $path);
|
||||
|
||||
// Add back the leading slash for the first segment if needed
|
||||
$result = [];
|
||||
$current = $isAbsolute ? '/' : '';
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
if ($segment === '') {
|
||||
// skip empty parts
|
||||
continue;
|
||||
}
|
||||
$current = rtrim($current, '/') . '/' . $segment;
|
||||
$result[] = $current;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Files\Sharing;
|
||||
|
||||
use Sabre\DAV\INode;
|
||||
use Sabre\DAVACL\AbstractPrincipalCollection;
|
||||
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
|
||||
|
||||
class RootCollection extends AbstractPrincipalCollection {
|
||||
public function __construct(
|
||||
private INode $root,
|
||||
BackendInterface $principalBackend,
|
||||
string $principalPrefix = 'principals',
|
||||
) {
|
||||
parent::__construct($principalBackend, $principalPrefix);
|
||||
}
|
||||
|
||||
public function getChildForPrincipal(array $principalInfo): INode {
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return 'files';
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,11 @@ class AddMissingIndicesListener implements IEventListener {
|
||||
'dav_shares_resourceid_access',
|
||||
['resourceid', 'access']
|
||||
);
|
||||
$event->addMissingIndex(
|
||||
'calendarobjects',
|
||||
'calobjects_by_uid_index',
|
||||
['calendarid', 'calendartype', 'uid']
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,82 +8,45 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace OCA\DAV\Migration;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCA\DAV\BackgroundJob\CleanupOrphanedChildrenJob;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\IRepairStep;
|
||||
|
||||
class RemoveOrphanEventsAndContacts implements IRepairStep {
|
||||
|
||||
public function __construct(
|
||||
private IDBConnection $connection,
|
||||
private readonly IJobList $jobList,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getName(): string {
|
||||
return 'Clean up orphan event and contact data';
|
||||
return 'Queue jobs to clean up orphan event and contact data';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function run(IOutput $output) {
|
||||
$orphanItems = $this->removeOrphanChildren('calendarobjects', 'calendars', 'calendarid');
|
||||
$output->info(sprintf('%d events without a calendar have been cleaned up', $orphanItems));
|
||||
$orphanItems = $this->removeOrphanChildren('calendarobjects_props', 'calendarobjects', 'objectid');
|
||||
$output->info(sprintf('%d properties without an events have been cleaned up', $orphanItems));
|
||||
$orphanItems = $this->removeOrphanChildren('calendarchanges', 'calendars', 'calendarid');
|
||||
$output->info(sprintf('%d changes without a calendar have been cleaned up', $orphanItems));
|
||||
public function run(IOutput $output): void {
|
||||
$this->queueJob('calendarobjects', 'calendars', 'calendarid', '%d events without a calendar have been cleaned up');
|
||||
$this->queueJob('calendarobjects_props', 'calendarobjects', 'objectid', '%d properties without an events have been cleaned up');
|
||||
$this->queueJob('calendarchanges', 'calendars', 'calendarid', '%d changes without a calendar have been cleaned up');
|
||||
|
||||
$orphanItems = $this->removeOrphanChildren('calendarobjects', 'calendarsubscriptions', 'calendarid');
|
||||
$output->info(sprintf('%d cached events without a calendar subscription have been cleaned up', $orphanItems));
|
||||
$orphanItems = $this->removeOrphanChildren('calendarchanges', 'calendarsubscriptions', 'calendarid');
|
||||
$output->info(sprintf('%d changes without a calendar subscription have been cleaned up', $orphanItems));
|
||||
$this->queueJob('calendarobjects', 'calendarsubscriptions', 'calendarid', '%d cached events without a calendar subscription have been cleaned up');
|
||||
$this->queueJob('calendarchanges', 'calendarsubscriptions', 'calendarid', '%d changes without a calendar subscription have been cleaned up');
|
||||
|
||||
$orphanItems = $this->removeOrphanChildren('cards', 'addressbooks', 'addressbookid');
|
||||
$output->info(sprintf('%d contacts without an addressbook have been cleaned up', $orphanItems));
|
||||
$orphanItems = $this->removeOrphanChildren('cards_properties', 'cards', 'cardid');
|
||||
$output->info(sprintf('%d properties without a contact have been cleaned up', $orphanItems));
|
||||
$orphanItems = $this->removeOrphanChildren('addressbookchanges', 'addressbooks', 'addressbookid');
|
||||
$output->info(sprintf('%d changes without an addressbook have been cleaned up', $orphanItems));
|
||||
$this->queueJob('cards', 'addressbooks', 'addressbookid', '%d contacts without an addressbook have been cleaned up');
|
||||
$this->queueJob('cards_properties', 'cards', 'cardid', '%d properties without a contact have been cleaned up');
|
||||
$this->queueJob('addressbookchanges', 'addressbooks', 'addressbookid', '%d changes without an addressbook have been cleaned up');
|
||||
}
|
||||
|
||||
protected function removeOrphanChildren($childTable, $parentTable, $parentId): int {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
|
||||
$qb->select('c.id')
|
||||
->from($childTable, 'c')
|
||||
->leftJoin('c', $parentTable, 'p', $qb->expr()->eq('c.' . $parentId, 'p.id'))
|
||||
->where($qb->expr()->isNull('p.id'));
|
||||
|
||||
if (\in_array($parentTable, ['calendars', 'calendarsubscriptions'], true)) {
|
||||
$calendarType = $parentTable === 'calendarsubscriptions' ? CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION : CalDavBackend::CALENDAR_TYPE_CALENDAR;
|
||||
$qb->andWhere($qb->expr()->eq('c.calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
|
||||
}
|
||||
|
||||
$result = $qb->executeQuery();
|
||||
|
||||
$orphanItems = [];
|
||||
while ($row = $result->fetch()) {
|
||||
$orphanItems[] = (int)$row['id'];
|
||||
}
|
||||
$result->closeCursor();
|
||||
|
||||
if (!empty($orphanItems)) {
|
||||
$qb->delete($childTable)
|
||||
->where($qb->expr()->in('id', $qb->createParameter('ids')));
|
||||
|
||||
$orphanItemsBatch = array_chunk($orphanItems, 200);
|
||||
foreach ($orphanItemsBatch as $items) {
|
||||
$qb->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY);
|
||||
$qb->executeStatement();
|
||||
}
|
||||
}
|
||||
|
||||
return count($orphanItems);
|
||||
private function queueJob(
|
||||
string $childTable,
|
||||
string $parentTable,
|
||||
string $parentId,
|
||||
string $logMessage,
|
||||
): void {
|
||||
$this->jobList->add(CleanupOrphanedChildrenJob::class, [
|
||||
CleanupOrphanedChildrenJob::ARGUMENT_CHILD_TABLE => $childTable,
|
||||
CleanupOrphanedChildrenJob::ARGUMENT_PARENT_TABLE => $parentTable,
|
||||
CleanupOrphanedChildrenJob::ARGUMENT_PARENT_ID => $parentId,
|
||||
CleanupOrphanedChildrenJob::ARGUMENT_LOG_MESSAGE => $logMessage,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ class Version1006Date20180628111625 extends SimpleMigrationStep {
|
||||
$calendarObjectsTable->dropIndex('calobjects_index');
|
||||
}
|
||||
$calendarObjectsTable->addUniqueIndex(['calendarid', 'calendartype', 'uri'], 'calobjects_index');
|
||||
$calendarObjectsTable->addUniqueIndex(['calendarid', 'calendartype', 'uid'], 'calobjects_by_uid_index');
|
||||
}
|
||||
|
||||
if ($schema->hasTable('calendarobjects_props')) {
|
||||
|
||||
@@ -160,6 +160,7 @@ class RootCollection extends SimpleCollection {
|
||||
Server::get(CleanupService::class),
|
||||
$rootFolder,
|
||||
$userSession,
|
||||
$shareManager,
|
||||
);
|
||||
$uploadCollection->disableListing = $disableListing;
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ use OCA\DAV\SystemTag\SystemTagPlugin;
|
||||
use OCA\DAV\Upload\ChunkingPlugin;
|
||||
use OCA\DAV\Upload\ChunkingV2Plugin;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\Accounts\IAccountManager;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
@@ -287,6 +288,7 @@ class Server {
|
||||
\OCP\Server::get(IPreview::class),
|
||||
\OCP\Server::get(IUserSession::class),
|
||||
\OCP\Server::get(IFilenameValidator::class),
|
||||
\OCP\Server::get(IAccountManager::class),
|
||||
false,
|
||||
$config->getSystemValueBool('debug', false) === false,
|
||||
)
|
||||
|
||||
@@ -10,20 +10,18 @@ namespace OCA\DAV\Upload;
|
||||
|
||||
use OCA\DAV\BackgroundJob\UploadCleanup;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class CleanupService {
|
||||
public function __construct(
|
||||
private IUserSession $userSession,
|
||||
private IJobList $jobList,
|
||||
) {
|
||||
}
|
||||
|
||||
public function addJob(string $folder) {
|
||||
$this->jobList->add(UploadCleanup::class, ['uid' => $this->userSession->getUser()->getUID(), 'folder' => $folder]);
|
||||
public function addJob(string $uid, string $folder) {
|
||||
$this->jobList->add(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]);
|
||||
}
|
||||
|
||||
public function removeJob(string $folder) {
|
||||
$this->jobList->remove(UploadCleanup::class, ['uid' => $this->userSession->getUser()->getUID(), 'folder' => $folder]);
|
||||
public function removeJob(string $uid, string $folder) {
|
||||
$this->jobList->remove(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace OCA\DAV\Upload;
|
||||
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Share\IManager;
|
||||
use Sabre\DAVACL\AbstractPrincipalCollection;
|
||||
use Sabre\DAVACL\PrincipalBackend;
|
||||
|
||||
@@ -22,6 +23,7 @@ class RootCollection extends AbstractPrincipalCollection {
|
||||
private CleanupService $cleanupService,
|
||||
private IRootFolder $rootFolder,
|
||||
private IUserSession $userSession,
|
||||
private IManager $shareManager,
|
||||
) {
|
||||
parent::__construct($principalBackend, $principalPrefix);
|
||||
}
|
||||
@@ -30,7 +32,13 @@ class RootCollection extends AbstractPrincipalCollection {
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getChildForPrincipal(array $principalInfo): UploadHome {
|
||||
return new UploadHome($principalInfo, $this->cleanupService, $this->rootFolder, $this->userSession);
|
||||
return new UploadHome(
|
||||
$principalInfo,
|
||||
$this->cleanupService,
|
||||
$this->rootFolder,
|
||||
$this->userSession,
|
||||
$this->shareManager,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ class UploadFolder implements ICollection {
|
||||
private Directory $node,
|
||||
private CleanupService $cleanupService,
|
||||
private IStorage $storage,
|
||||
private string $uid,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ class UploadFolder implements ICollection {
|
||||
$this->node->delete();
|
||||
|
||||
// Background cleanup job is not needed anymore
|
||||
$this->cleanupService->removeJob($this->getName());
|
||||
$this->cleanupService->removeJob($this->uid, $this->getName());
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
|
||||
@@ -17,6 +17,7 @@ use Sabre\DAV\Exception\Forbidden;
|
||||
use Sabre\DAV\ICollection;
|
||||
|
||||
class UploadHome implements ICollection {
|
||||
private string $uid;
|
||||
private ?Folder $uploadFolder = null;
|
||||
|
||||
public function __construct(
|
||||
@@ -24,7 +25,19 @@ class UploadHome implements ICollection {
|
||||
private readonly CleanupService $cleanupService,
|
||||
private readonly IRootFolder $rootFolder,
|
||||
private readonly IUserSession $userSession,
|
||||
private readonly \OCP\Share\IManager $shareManager,
|
||||
) {
|
||||
[$prefix, $name] = \Sabre\Uri\split($principalInfo['uri']);
|
||||
if ($prefix === 'principals/shares') {
|
||||
$this->uid = $this->shareManager->getShareByToken($name)->getShareOwner();
|
||||
} else {
|
||||
$user = $this->userSession->getUser();
|
||||
if (!$user) {
|
||||
throw new Forbidden('Not logged in');
|
||||
}
|
||||
|
||||
$this->uid = $user->getUID();
|
||||
}
|
||||
}
|
||||
|
||||
public function createFile($name, $data = null) {
|
||||
@@ -35,16 +48,26 @@ class UploadHome implements ICollection {
|
||||
$this->impl()->createDirectory($name);
|
||||
|
||||
// Add a cleanup job
|
||||
$this->cleanupService->addJob($name);
|
||||
$this->cleanupService->addJob($this->uid, $name);
|
||||
}
|
||||
|
||||
public function getChild($name): UploadFolder {
|
||||
return new UploadFolder($this->impl()->getChild($name), $this->cleanupService, $this->getStorage());
|
||||
return new UploadFolder(
|
||||
$this->impl()->getChild($name),
|
||||
$this->cleanupService,
|
||||
$this->getStorage(),
|
||||
$this->uid,
|
||||
);
|
||||
}
|
||||
|
||||
public function getChildren(): array {
|
||||
return array_map(function ($node) {
|
||||
return new UploadFolder($node, $this->cleanupService, $this->getStorage());
|
||||
return new UploadFolder(
|
||||
$node,
|
||||
$this->cleanupService,
|
||||
$this->getStorage(),
|
||||
$this->uid,
|
||||
);
|
||||
}, $this->impl()->getChildren());
|
||||
}
|
||||
|
||||
@@ -71,11 +94,7 @@ class UploadHome implements ICollection {
|
||||
|
||||
private function getUploadFolder(): Folder {
|
||||
if ($this->uploadFolder === null) {
|
||||
$user = $this->userSession->getUser();
|
||||
if (!$user) {
|
||||
throw new Forbidden('Not logged in');
|
||||
}
|
||||
$path = '/' . $user->getUID() . '/uploads';
|
||||
$path = '/' . $this->uid . '/uploads';
|
||||
try {
|
||||
$folder = $this->rootFolder->get($path);
|
||||
if (!$folder instanceof Folder) {
|
||||
|
||||
@@ -29,12 +29,16 @@
|
||||
"dav": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"chunking"
|
||||
"chunking",
|
||||
"public_shares_chunking"
|
||||
],
|
||||
"properties": {
|
||||
"chunking": {
|
||||
"type": "string"
|
||||
},
|
||||
"public_shares_chunking": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"bulkupload": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
script('dav', 'settings-example-content');
|
||||
\OCP\Util::addScript('dav', 'settings-example-content', 'core');
|
||||
|
||||
?>
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\integration\DAV\Sharing;
|
||||
|
||||
use OC\Memcache\NullCache;
|
||||
use OCA\DAV\CalDAV\Calendar;
|
||||
use OCA\DAV\CalDAV\Sharing\Service;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\Sharing\Backend;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCA\DAV\DAV\Sharing\SharingService;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Server;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
/**
|
||||
* @group DB
|
||||
*/
|
||||
class CalDavSharingBackendTest extends TestCase {
|
||||
|
||||
private IDBConnection $db;
|
||||
private IUserManager $userManager;
|
||||
private IGroupManager $groupManager;
|
||||
private Principal $principalBackend;
|
||||
private ICacheFactory $cacheFactory;
|
||||
private LoggerInterface $logger;
|
||||
private SharingMapper $sharingMapper;
|
||||
private SharingService $sharingService;
|
||||
private Backend $sharingBackend;
|
||||
|
||||
private $resourceIds = [10001];
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->db = Server::get(IDBConnection::class);
|
||||
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->groupManager = $this->createMock(IGroupManager::class);
|
||||
$this->principalBackend = $this->createMock(Principal::class);
|
||||
$this->cacheFactory = $this->createMock(ICacheFactory::class);
|
||||
$this->cacheFactory->method('createInMemory')
|
||||
->willReturn(new NullCache());
|
||||
$this->logger = new \Psr\Log\NullLogger();
|
||||
|
||||
$this->sharingMapper = new SharingMapper($this->db);
|
||||
$this->sharingService = new Service($this->sharingMapper);
|
||||
|
||||
$this->sharingBackend = new \OCA\DAV\CalDAV\Sharing\Backend(
|
||||
$this->userManager,
|
||||
$this->groupManager,
|
||||
$this->principalBackend,
|
||||
$this->cacheFactory,
|
||||
$this->sharingService,
|
||||
$this->logger
|
||||
);
|
||||
|
||||
$this->removeFixtures();
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
$this->removeFixtures();
|
||||
}
|
||||
|
||||
protected function removeFixtures(): void {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->delete('dav_shares')
|
||||
->where($qb->expr()->in('resourceid', $qb->createNamedParameter($this->resourceIds, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
public function testShareCalendarWithGroup(): void {
|
||||
$calendar = $this->createMock(Calendar::class);
|
||||
$calendar->method('getResourceId')
|
||||
->willReturn(10001);
|
||||
$calendar->method('getOwner')
|
||||
->willReturn('principals/users/admin');
|
||||
|
||||
$this->principalBackend->method('findByUri')
|
||||
->willReturn('principals/groups/alice_bob');
|
||||
|
||||
$this->groupManager->method('groupExists')
|
||||
->willReturn(true);
|
||||
|
||||
$this->sharingBackend->updateShares(
|
||||
$calendar,
|
||||
[['href' => 'principals/groups/alice_bob']],
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
$this->assertCount(1, $this->sharingService->getShares(10001));
|
||||
}
|
||||
|
||||
public function testUnshareCalendarFromGroup(): void {
|
||||
$calendar = $this->createMock(Calendar::class);
|
||||
$calendar->method('getResourceId')
|
||||
->willReturn(10001);
|
||||
$calendar->method('getOwner')
|
||||
->willReturn('principals/users/admin');
|
||||
|
||||
$this->principalBackend->method('findByUri')
|
||||
->willReturn('principals/groups/alice_bob');
|
||||
|
||||
$this->groupManager->method('groupExists')
|
||||
->willReturn(true);
|
||||
|
||||
$this->sharingBackend->updateShares(
|
||||
shareable: $calendar,
|
||||
add: [['href' => 'principals/groups/alice_bob']],
|
||||
remove: [],
|
||||
);
|
||||
|
||||
$this->assertCount(1, $this->sharingService->getShares(10001));
|
||||
|
||||
$this->sharingBackend->updateShares(
|
||||
shareable: $calendar,
|
||||
add: [],
|
||||
remove: ['principals/groups/alice_bob'],
|
||||
);
|
||||
|
||||
$this->assertCount(0, $this->sharingService->getShares(10001));
|
||||
}
|
||||
|
||||
public function testShareCalendarWithGroupAndUnshareAsUser(): void {
|
||||
$calendar = $this->createMock(Calendar::class);
|
||||
$calendar->method('getResourceId')
|
||||
->willReturn(10001);
|
||||
$calendar->method('getOwner')
|
||||
->willReturn('principals/users/admin');
|
||||
|
||||
$this->principalBackend->method('findByUri')
|
||||
->willReturnMap([
|
||||
['principals/groups/alice_bob', '', 'principals/groups/alice_bob'],
|
||||
['principals/users/bob', '', 'principals/users/bob'],
|
||||
]);
|
||||
$this->principalBackend->method('getGroupMembership')
|
||||
->willReturn([
|
||||
'principals/groups/alice_bob',
|
||||
]);
|
||||
$this->principalBackend->method('getCircleMembership')
|
||||
->willReturn([]);
|
||||
|
||||
$this->groupManager->method('groupExists')
|
||||
->willReturn(true);
|
||||
|
||||
/*
|
||||
* Owner is sharing the calendar with a group.
|
||||
*/
|
||||
$this->sharingBackend->updateShares(
|
||||
shareable: $calendar,
|
||||
add: [['href' => 'principals/groups/alice_bob']],
|
||||
remove: [],
|
||||
);
|
||||
|
||||
$this->assertCount(1, $this->sharingService->getShares(10001));
|
||||
|
||||
/*
|
||||
* Member of the group unshares the calendar.
|
||||
*/
|
||||
$this->sharingBackend->unshare(
|
||||
shareable: $calendar,
|
||||
principalUri: 'principals/users/bob'
|
||||
);
|
||||
|
||||
$this->assertCount(1, $this->sharingService->getShares(10001));
|
||||
$this->assertCount(1, $this->sharingService->getUnshares(10001));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the functionality of sharing a calendar with a user, then with a group (that includes the shared user),
|
||||
* and subsequently unsharing it from the individual user. Verifies that the unshare operation correctly removes the specific user share
|
||||
* without creating an additional unshare entry.
|
||||
*/
|
||||
public function testShareCalendarWithUserThenGroupThenUnshareUser(): void {
|
||||
$calendar = $this->createMock(Calendar::class);
|
||||
$calendar->method('getResourceId')
|
||||
->willReturn(10001);
|
||||
$calendar->method('getOwner')
|
||||
->willReturn('principals/users/admin');
|
||||
|
||||
$this->principalBackend->method('findByUri')
|
||||
->willReturnMap([
|
||||
['principals/groups/alice_bob', '', 'principals/groups/alice_bob'],
|
||||
['principals/users/bob', '', 'principals/users/bob'],
|
||||
]);
|
||||
$this->principalBackend->method('getGroupMembership')
|
||||
->willReturn([
|
||||
'principals/groups/alice_bob',
|
||||
]);
|
||||
$this->principalBackend->method('getCircleMembership')
|
||||
->willReturn([]);
|
||||
|
||||
$this->userManager->method('userExists')
|
||||
->willReturn(true);
|
||||
$this->groupManager->method('groupExists')
|
||||
->willReturn(true);
|
||||
|
||||
/*
|
||||
* Step 1) The owner shares the calendar with a user.
|
||||
*/
|
||||
$this->sharingBackend->updateShares(
|
||||
shareable: $calendar,
|
||||
add: [['href' => 'principals/users/bob']],
|
||||
remove: [],
|
||||
);
|
||||
|
||||
$this->assertCount(1, $this->sharingService->getShares(10001));
|
||||
|
||||
/*
|
||||
* Step 2) The owner shares the calendar with a group that includes the
|
||||
* user from step 1 as a member.
|
||||
*/
|
||||
$this->sharingBackend->updateShares(
|
||||
shareable: $calendar,
|
||||
add: [['href' => 'principals/groups/alice_bob']],
|
||||
remove: [],
|
||||
);
|
||||
|
||||
$this->assertCount(2, $this->sharingService->getShares(10001));
|
||||
|
||||
/*
|
||||
* Step 3) Unshare the calendar from user as owner.
|
||||
*/
|
||||
$this->sharingBackend->updateShares(
|
||||
shareable: $calendar,
|
||||
add: [],
|
||||
remove: ['principals/users/bob'],
|
||||
);
|
||||
|
||||
/*
|
||||
* The purpose of this test is to ensure that removing a user from a share, as the owner, does not result in an "unshare" row being added.
|
||||
* Instead, the actual user share should be removed.
|
||||
*/
|
||||
$this->assertCount(1, $this->sharingService->getShares(10001));
|
||||
$this->assertCount(0, $this->sharingService->getUnshares(10001));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,8 @@ declare(strict_types=1);
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\integration\DAV\Sharing;
|
||||
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\Server;
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\BackgroundJob;
|
||||
|
||||
use OCA\DAV\BackgroundJob\CleanupOrphanedChildrenJob;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\DB\IResult;
|
||||
use OCP\DB\QueryBuilder\IExpressionBuilder;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
class CleanupOrphanedChildrenJobTest extends TestCase {
|
||||
private CleanupOrphanedChildrenJob $job;
|
||||
|
||||
private ITimeFactory&MockObject $timeFactory;
|
||||
private IDBConnection&MockObject $connection;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private IJobList&MockObject $jobList;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->timeFactory = $this->createMock(ITimeFactory::class);
|
||||
$this->connection = $this->createMock(IDBConnection::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->jobList = $this->createMock(IJobList::class);
|
||||
|
||||
$this->job = new CleanupOrphanedChildrenJob(
|
||||
$this->timeFactory,
|
||||
$this->connection,
|
||||
$this->logger,
|
||||
$this->jobList,
|
||||
);
|
||||
}
|
||||
|
||||
private function getArgument(): array {
|
||||
return [
|
||||
'childTable' => 'childTable',
|
||||
'parentTable' => 'parentTable',
|
||||
'parentId' => 'parentId',
|
||||
'logMessage' => 'logMessage',
|
||||
];
|
||||
}
|
||||
|
||||
private function getMockQueryBuilder(): IQueryBuilder&MockObject {
|
||||
$expr = $this->createMock(IExpressionBuilder::class);
|
||||
$qb = $this->createMock(IQueryBuilder::class);
|
||||
$qb->method('select')
|
||||
->willReturnSelf();
|
||||
$qb->method('from')
|
||||
->willReturnSelf();
|
||||
$qb->method('leftJoin')
|
||||
->willReturnSelf();
|
||||
$qb->method('where')
|
||||
->willReturnSelf();
|
||||
$qb->method('setMaxResults')
|
||||
->willReturnSelf();
|
||||
$qb->method('andWhere')
|
||||
->willReturnSelf();
|
||||
$qb->method('expr')
|
||||
->willReturn($expr);
|
||||
$qb->method('delete')
|
||||
->willReturnSelf();
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function testRunWithoutOrphans(): void {
|
||||
$argument = $this->getArgument();
|
||||
$selectQb = $this->getMockQueryBuilder();
|
||||
$result = $this->createMock(IResult::class);
|
||||
|
||||
$this->connection->expects(self::once())
|
||||
->method('getQueryBuilder')
|
||||
->willReturn($selectQb);
|
||||
$selectQb->expects(self::once())
|
||||
->method('executeQuery')
|
||||
->willReturn($result);
|
||||
$result->expects(self::once())
|
||||
->method('fetchAll')
|
||||
->willReturn([]);
|
||||
$result->expects(self::once())
|
||||
->method('closeCursor');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
self::invokePrivate($this->job, 'run', [$argument]);
|
||||
}
|
||||
|
||||
public function testRunWithPartialBatch(): void {
|
||||
$argument = $this->getArgument();
|
||||
$selectQb = $this->getMockQueryBuilder();
|
||||
$deleteQb = $this->getMockQueryBuilder();
|
||||
$result = $this->createMock(IResult::class);
|
||||
|
||||
$qbInvocationCount = self::exactly(2);
|
||||
$this->connection->expects($qbInvocationCount)
|
||||
->method('getQueryBuilder')
|
||||
->willReturnCallback(function () use ($qbInvocationCount, $selectQb, $deleteQb) {
|
||||
return match ($qbInvocationCount->getInvocationCount()) {
|
||||
1 => $selectQb,
|
||||
2 => $deleteQb,
|
||||
};
|
||||
});
|
||||
$selectQb->expects(self::once())
|
||||
->method('executeQuery')
|
||||
->willReturn($result);
|
||||
$result->expects(self::once())
|
||||
->method('fetchAll')
|
||||
->willReturn([
|
||||
['id' => 42],
|
||||
['id' => 43],
|
||||
]);
|
||||
$result->expects(self::once())
|
||||
->method('closeCursor');
|
||||
$deleteQb->expects(self::once())
|
||||
->method('delete')
|
||||
->willReturnSelf();
|
||||
$deleteQb->expects(self::once())
|
||||
->method('executeStatement');
|
||||
$this->jobList->expects(self::never())
|
||||
->method('add');
|
||||
|
||||
self::invokePrivate($this->job, 'run', [$argument]);
|
||||
}
|
||||
|
||||
public function testRunWithFullBatch(): void {
|
||||
$argument = $this->getArgument();
|
||||
$selectQb = $this->getMockQueryBuilder();
|
||||
$deleteQb = $this->getMockQueryBuilder();
|
||||
$result = $this->createMock(IResult::class);
|
||||
|
||||
$qbInvocationCount = self::exactly(2);
|
||||
$this->connection->expects($qbInvocationCount)
|
||||
->method('getQueryBuilder')
|
||||
->willReturnCallback(function () use ($qbInvocationCount, $selectQb, $deleteQb) {
|
||||
return match ($qbInvocationCount->getInvocationCount()) {
|
||||
1 => $selectQb,
|
||||
2 => $deleteQb,
|
||||
};
|
||||
});
|
||||
$selectQb->expects(self::once())
|
||||
->method('executeQuery')
|
||||
->willReturn($result);
|
||||
$result->expects(self::once())
|
||||
->method('fetchAll')
|
||||
->willReturn(array_map(static fn ($i) => ['id' => 42 + $i], range(0, 999)));
|
||||
$result->expects(self::once())
|
||||
->method('closeCursor');
|
||||
$deleteQb->expects(self::once())
|
||||
->method('delete')
|
||||
->willReturnSelf();
|
||||
$deleteQb->expects(self::once())
|
||||
->method('executeStatement');
|
||||
$this->jobList->expects(self::once())
|
||||
->method('add')
|
||||
->with(CleanupOrphanedChildrenJob::class, $argument);
|
||||
|
||||
self::invokePrivate($this->job, 'run', [$argument]);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use OCA\DAV\DAV\Sharing\Plugin as SharingPlugin;
|
||||
use OCA\DAV\Events\CalendarDeletedEvent;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use Psr\Log\NullLogger;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\PropPatch;
|
||||
use Sabre\DAV\Xml\Property\Href;
|
||||
@@ -519,7 +520,7 @@ EOD;
|
||||
sort($stateLive['deleted']);
|
||||
// test live state
|
||||
$this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with events in calendar');
|
||||
|
||||
|
||||
/** modify/delete events in calendar */
|
||||
$this->deleteEvent($calendarId, $event1);
|
||||
$this->modifyEvent($calendarId, $event2, '20250701T140000Z', '20250701T150000Z');
|
||||
@@ -1847,4 +1848,46 @@ EOD;
|
||||
$this->assertEquals('Missing DTSTART 1', $results[2]['objects'][0]['SUMMARY'][0]);
|
||||
$this->assertEquals('Missing DTSTART 2', $results[3]['objects'][0]['SUMMARY'][0]);
|
||||
}
|
||||
|
||||
public function testUnshare(): void {
|
||||
$principalGroup = 'principal:' . self::UNIT_TEST_GROUP;
|
||||
$principalUser = 'principal:' . self::UNIT_TEST_USER;
|
||||
|
||||
$l10n = $this->createMock(IL10N::class);
|
||||
$l10n->method('t')
|
||||
->willReturnCallback(fn ($text, $parameters = []) => vsprintf($text, $parameters));
|
||||
$config = $this->createMock(IConfig::class);
|
||||
$logger = new NullLogger();
|
||||
|
||||
$this->principal->expects($this->exactly(2))
|
||||
->method('findByUri')
|
||||
->willReturnMap([
|
||||
[$principalGroup, '', self::UNIT_TEST_GROUP],
|
||||
[$principalUser, '', self::UNIT_TEST_USER],
|
||||
]);
|
||||
$this->groupManager->expects($this->once())
|
||||
->method('groupExists')
|
||||
->willReturn(true);
|
||||
$this->dispatcher->expects($this->exactly(2))
|
||||
->method('dispatchTyped');
|
||||
|
||||
$calendarId = $this->createTestCalendar();
|
||||
$calendarInfo = $this->backend->getCalendarById($calendarId);
|
||||
|
||||
$calendar = new Calendar($this->backend, $calendarInfo, $l10n, $config, $logger);
|
||||
|
||||
$this->backend->updateShares(
|
||||
shareable: $calendar,
|
||||
add: [
|
||||
['href' => $principalGroup, 'readOnly' => false]
|
||||
],
|
||||
remove: []
|
||||
);
|
||||
|
||||
$this->backend->unshare(
|
||||
shareable: $calendar,
|
||||
principal: $principalUser
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
namespace OCA\DAV\Tests\unit\CalDAV;
|
||||
|
||||
use Generator;
|
||||
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Calendar;
|
||||
@@ -20,24 +21,19 @@ use Sabre\VObject\ITip\Message;
|
||||
use Sabre\VObject\Reader;
|
||||
|
||||
class CalendarImplTest extends \Test\TestCase {
|
||||
/** @var CalendarImpl */
|
||||
private $calendarImpl;
|
||||
|
||||
/** @var Calendar | \PHPUnit\Framework\MockObject\MockObject */
|
||||
private $calendar;
|
||||
|
||||
/** @var array */
|
||||
private $calendarInfo;
|
||||
|
||||
/** @var CalDavBackend | \PHPUnit\Framework\MockObject\MockObject */
|
||||
private $backend;
|
||||
private Calendar|MockObject $calendar;
|
||||
private array $calendarInfo;
|
||||
private CalDavBackend|MockObject $backend;
|
||||
private CalendarImpl|MockObject $calendarImpl;
|
||||
private array $mockExportCollection;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->calendar = $this->createMock(Calendar::class);
|
||||
$this->calendarInfo = [
|
||||
'id' => 'fancy_id_123',
|
||||
'id' => 1,
|
||||
'{DAV:}displayname' => 'user readable name 123',
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
|
||||
'uri' => '/this/is/a/uri',
|
||||
@@ -45,13 +41,16 @@ class CalendarImplTest extends \Test\TestCase {
|
||||
];
|
||||
$this->backend = $this->createMock(CalDavBackend::class);
|
||||
|
||||
$this->calendarImpl = new CalendarImpl($this->calendar,
|
||||
$this->calendarInfo, $this->backend);
|
||||
$this->calendarImpl = new CalendarImpl(
|
||||
$this->calendar,
|
||||
$this->calendarInfo,
|
||||
$this->backend
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function testGetKey(): void {
|
||||
$this->assertEquals($this->calendarImpl->getKey(), 'fancy_id_123');
|
||||
$this->assertEquals($this->calendarImpl->getKey(), 1);
|
||||
}
|
||||
|
||||
public function testGetDisplayname(): void {
|
||||
@@ -261,4 +260,48 @@ EOF;
|
||||
$iTipMessage->message = $vObject;
|
||||
return $iTipMessage;
|
||||
}
|
||||
|
||||
protected function mockExportGenerator(): Generator {
|
||||
foreach ($this->mockExportCollection as $entry) {
|
||||
yield $entry;
|
||||
}
|
||||
}
|
||||
|
||||
public function testExport(): void {
|
||||
// Arrange
|
||||
// construct calendar with a 1 hour event and same start/end time zones
|
||||
$vCalendar = new VCalendar();
|
||||
/** @var VEvent $vEvent */
|
||||
$vEvent = $vCalendar->add('VEVENT', []);
|
||||
$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('SUMMARY', 'Test Recurrence Event');
|
||||
$vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
|
||||
$vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
// construct data store return
|
||||
$this->mockExportCollection[] = [
|
||||
'id' => 1,
|
||||
'calendardata' => $vCalendar->serialize()
|
||||
];
|
||||
$this->backend->expects($this->once())
|
||||
->method('exportCalendar')
|
||||
->with(1, $this->backend::CALENDAR_TYPE_CALENDAR, null)
|
||||
->willReturn($this->mockExportGenerator());
|
||||
|
||||
// Act
|
||||
foreach ($this->calendarImpl->export(null) as $entry) {
|
||||
$exported[] = $entry;
|
||||
}
|
||||
|
||||
// Assert
|
||||
$this->assertCount(1, $exported, 'Invalid exported items count');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,12 +43,13 @@ class CalendarTest extends TestCase {
|
||||
}
|
||||
|
||||
public function testDelete(): void {
|
||||
/** @var MockObject | CalDavBackend $backend */
|
||||
$backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
|
||||
$backend->expects($this->once())->method('updateShares');
|
||||
$backend->expects($this->any())->method('getShares')->willReturn([
|
||||
['href' => 'principal:user2']
|
||||
]);
|
||||
/** @var CalDavBackend&MockObject $backend */
|
||||
$backend = $this->createMock(CalDavBackend::class);
|
||||
$backend->expects($this->never())
|
||||
->method('updateShares');
|
||||
$backend->expects($this->once())
|
||||
->method('unshare');
|
||||
|
||||
$calendarInfo = [
|
||||
'{http://owncloud.org/ns}owner-principal' => 'user1',
|
||||
'principaluri' => 'user2',
|
||||
@@ -61,12 +62,13 @@ class CalendarTest extends TestCase {
|
||||
|
||||
|
||||
public function testDeleteFromGroup(): void {
|
||||
/** @var MockObject | CalDavBackend $backend */
|
||||
$backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock();
|
||||
$backend->expects($this->once())->method('updateShares');
|
||||
$backend->expects($this->any())->method('getShares')->willReturn([
|
||||
['href' => 'principal:group2']
|
||||
]);
|
||||
/** @var CalDavBackend&MockObject $backend */
|
||||
$backend = $this->createMock(CalDavBackend::class);
|
||||
$backend->expects($this->never())
|
||||
->method('updateShares');
|
||||
$backend->expects($this->once())
|
||||
->method('unshare');
|
||||
|
||||
$calendarInfo = [
|
||||
'{http://owncloud.org/ns}owner-principal' => 'user1',
|
||||
'principaluri' => 'user2',
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Export;
|
||||
|
||||
use Generator;
|
||||
use OCA\DAV\CalDAV\Export\ExportService;
|
||||
use OCP\Calendar\CalendarExportOptions;
|
||||
use OCP\Calendar\ICalendarExport;
|
||||
use OCP\ServerVersion;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
|
||||
class ExportServiceTest extends \Test\TestCase {
|
||||
|
||||
private ServerVersion|MockObject $serverVersion;
|
||||
private ExportService $service;
|
||||
private ICalendarExport|MockObject $calendar;
|
||||
private array $mockExportCollection;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->serverVersion = $this->createMock(ServerVersion::class);
|
||||
$this->serverVersion->method('getVersionString')
|
||||
->willReturn('32.0.0.0');
|
||||
$this->service = new ExportService($this->serverVersion);
|
||||
$this->calendar = $this->createMock(ICalendarExport::class);
|
||||
|
||||
}
|
||||
|
||||
protected function mockGenerator(): Generator {
|
||||
foreach ($this->mockExportCollection as $entry) {
|
||||
yield $entry;
|
||||
}
|
||||
}
|
||||
|
||||
public function testExport(): void {
|
||||
// Arrange
|
||||
// construct calendar with a 1 hour event and same start/end time zones
|
||||
$vCalendar = new VCalendar();
|
||||
/** @var \Sabre\VObject\Component\VEvent $vEvent */
|
||||
$vEvent = $vCalendar->add('VEVENT', []);
|
||||
$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
|
||||
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
|
||||
$vEvent->add('SUMMARY', 'Test Recurrence Event');
|
||||
$vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
|
||||
$vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
|
||||
'CN' => 'Attendee One',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'RSVP' => 'TRUE'
|
||||
]);
|
||||
// construct calendar return
|
||||
$options = new CalendarExportOptions();
|
||||
$this->mockExportCollection[] = $vCalendar;
|
||||
$this->calendar->expects($this->once())
|
||||
->method('export')
|
||||
->with($options)
|
||||
->willReturn($this->mockGenerator());
|
||||
|
||||
// Act
|
||||
$document = '';
|
||||
foreach ($this->service->export($this->calendar, $options) as $chunk) {
|
||||
$document .= $chunk;
|
||||
}
|
||||
|
||||
// Assert
|
||||
$this->assertStringContainsString('BEGIN:VCALENDAR', $document, 'Exported document calendar start missing');
|
||||
$this->assertStringContainsString('BEGIN:VEVENT', $document, 'Exported document event start missing');
|
||||
$this->assertStringContainsString('END:VEVENT', $document, 'Exported document event end missing');
|
||||
$this->assertStringContainsString('END:VCALENDAR', $document, 'Exported document calendar end missing');
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ class CapabilitiesTest extends TestCase {
|
||||
$expected = [
|
||||
'dav' => [
|
||||
'chunking' => '1.0',
|
||||
'public_shares_chunking' => true,
|
||||
],
|
||||
];
|
||||
$this->assertSame($expected, $capabilities->getCapabilities());
|
||||
@@ -47,6 +48,7 @@ class CapabilitiesTest extends TestCase {
|
||||
$expected = [
|
||||
'dav' => [
|
||||
'chunking' => '1.0',
|
||||
'public_shares_chunking' => true,
|
||||
'bulkupload' => '1.0',
|
||||
],
|
||||
];
|
||||
@@ -67,6 +69,7 @@ class CapabilitiesTest extends TestCase {
|
||||
$expected = [
|
||||
'dav' => [
|
||||
'chunking' => '1.0',
|
||||
'public_shares_chunking' => true,
|
||||
'absence-supported' => true,
|
||||
'absence-replacement' => true,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\Command;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\Command\ListCalendarShares;
|
||||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\DAV\Sharing\SharingMapper;
|
||||
use OCP\IUserManager;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Test\TestCase;
|
||||
|
||||
class ListCalendarSharesTest extends TestCase {
|
||||
|
||||
private IUserManager&MockObject $userManager;
|
||||
private Principal&MockObject $principal;
|
||||
private CalDavBackend&MockObject $caldav;
|
||||
private SharingMapper $sharingMapper;
|
||||
private ListCalendarShares $command;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->principal = $this->createMock(Principal::class);
|
||||
$this->caldav = $this->createMock(CalDavBackend::class);
|
||||
$this->sharingMapper = $this->createMock(SharingMapper::class);
|
||||
|
||||
$this->command = new ListCalendarShares(
|
||||
$this->userManager,
|
||||
$this->principal,
|
||||
$this->caldav,
|
||||
$this->sharingMapper,
|
||||
);
|
||||
}
|
||||
|
||||
public function testUserUnknown(): void {
|
||||
$user = 'bob';
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage("User $user is unknown");
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('userExists')
|
||||
->with($user)
|
||||
->willReturn(false);
|
||||
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute([
|
||||
'uid' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testPrincipalNotFound(): void {
|
||||
$user = 'bob';
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage("Unable to fetch principal for user $user");
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('userExists')
|
||||
->with($user)
|
||||
->willReturn(true);
|
||||
|
||||
$this->principal->expects($this->once())
|
||||
->method('getPrincipalByPath')
|
||||
->with('principals/users/' . $user)
|
||||
->willReturn(null);
|
||||
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute([
|
||||
'uid' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testNoCalendarShares(): void {
|
||||
$user = 'bob';
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('userExists')
|
||||
->with($user)
|
||||
->willReturn(true);
|
||||
|
||||
$this->principal->expects($this->once())
|
||||
->method('getPrincipalByPath')
|
||||
->with('principals/users/' . $user)
|
||||
->willReturn([
|
||||
'uri' => 'principals/users/' . $user,
|
||||
]);
|
||||
|
||||
$this->principal->expects($this->once())
|
||||
->method('getGroupMembership')
|
||||
->willReturn([]);
|
||||
$this->principal->expects($this->once())
|
||||
->method('getCircleMembership')
|
||||
->willReturn([]);
|
||||
|
||||
$this->sharingMapper->expects($this->once())
|
||||
->method('getSharesByPrincipals')
|
||||
->willReturn([]);
|
||||
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute([
|
||||
'uid' => $user,
|
||||
]);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
"User $user has no calendar shares",
|
||||
$commandTester->getDisplay()
|
||||
);
|
||||
}
|
||||
|
||||
public function testFilterByCalendarId(): void {
|
||||
$user = 'bob';
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('userExists')
|
||||
->with($user)
|
||||
->willReturn(true);
|
||||
|
||||
$this->principal->expects($this->once())
|
||||
->method('getPrincipalByPath')
|
||||
->with('principals/users/' . $user)
|
||||
->willReturn([
|
||||
'uri' => 'principals/users/' . $user,
|
||||
]);
|
||||
|
||||
$this->principal->expects($this->once())
|
||||
->method('getGroupMembership')
|
||||
->willReturn([]);
|
||||
$this->principal->expects($this->once())
|
||||
->method('getCircleMembership')
|
||||
->willReturn([]);
|
||||
|
||||
$this->sharingMapper->expects($this->once())
|
||||
->method('getSharesByPrincipals')
|
||||
->willReturn([
|
||||
[
|
||||
'id' => 1000,
|
||||
'principaluri' => 'principals/users/bob',
|
||||
'type' => 'calendar',
|
||||
'access' => 2,
|
||||
'resourceid' => 10
|
||||
],
|
||||
[
|
||||
'id' => 1001,
|
||||
'principaluri' => 'principals/users/bob',
|
||||
'type' => 'calendar',
|
||||
'access' => 3,
|
||||
'resourceid' => 11
|
||||
],
|
||||
]);
|
||||
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute([
|
||||
'uid' => $user,
|
||||
'--calendar-id' => 10,
|
||||
]);
|
||||
|
||||
$this->assertStringNotContainsString(
|
||||
'1001',
|
||||
$commandTester->getDisplay()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use OC\Files\View;
|
||||
use OCA\DAV\Connector\Sabre\Directory;
|
||||
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
|
||||
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
|
||||
use OCA\Files_Sharing\External\Storage;
|
||||
use OCP\Constants;
|
||||
use OCP\Files\ForbiddenException;
|
||||
use OCP\Files\InvalidPathException;
|
||||
@@ -286,7 +287,7 @@ class DirectoryTest extends \Test\TestCase {
|
||||
->willReturnMap([
|
||||
['\OCA\Files_Sharing\SharedStorage', false],
|
||||
['\OC\Files\Storage\Wrapper\Quota', false],
|
||||
[\OCA\Files_Sharing\External\Storage::class, false],
|
||||
[Storage::class, false],
|
||||
]);
|
||||
|
||||
$storage->expects($this->once())
|
||||
@@ -342,7 +343,7 @@ class DirectoryTest extends \Test\TestCase {
|
||||
->willReturnMap([
|
||||
['\OCA\Files_Sharing\SharedStorage', false],
|
||||
['\OC\Files\Storage\Wrapper\Quota', true],
|
||||
[\OCA\Files_Sharing\External\Storage::class, false],
|
||||
[Storage::class, false],
|
||||
]);
|
||||
|
||||
$storage->expects($this->once())
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
*/
|
||||
namespace OCA\DAV\Tests\unit\Connector\Sabre;
|
||||
|
||||
use OC\Accounts\Account;
|
||||
use OC\Accounts\AccountProperty;
|
||||
use OC\User\User;
|
||||
use OCA\DAV\Connector\Sabre\Directory;
|
||||
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
|
||||
use OCA\DAV\Connector\Sabre\File;
|
||||
use OCA\DAV\Connector\Sabre\FilesPlugin;
|
||||
use OCA\DAV\Connector\Sabre\Node;
|
||||
use OCP\Accounts\IAccountManager;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\Files\IFilenameValidator;
|
||||
use OCP\Files\InvalidPathException;
|
||||
@@ -43,6 +46,7 @@ class FilesPluginTest extends TestCase {
|
||||
private IPreview&MockObject $previewManager;
|
||||
private IUserSession&MockObject $userSession;
|
||||
private IFilenameValidator&MockObject $filenameValidator;
|
||||
private IAccountManager&MockObject $accountManager;
|
||||
private FilesPlugin $plugin;
|
||||
|
||||
protected function setUp(): void {
|
||||
@@ -57,6 +61,7 @@ class FilesPluginTest extends TestCase {
|
||||
$this->previewManager = $this->createMock(IPreview::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->filenameValidator = $this->createMock(IFilenameValidator::class);
|
||||
$this->accountManager = $this->createMock(IAccountManager::class);
|
||||
|
||||
$this->plugin = new FilesPlugin(
|
||||
$this->tree,
|
||||
@@ -65,6 +70,7 @@ class FilesPluginTest extends TestCase {
|
||||
$this->previewManager,
|
||||
$this->userSession,
|
||||
$this->filenameValidator,
|
||||
$this->accountManager,
|
||||
);
|
||||
|
||||
$response = $this->getMockBuilder(ResponseInterface::class)
|
||||
@@ -154,6 +160,13 @@ class FilesPluginTest extends TestCase {
|
||||
->method('getDisplayName')
|
||||
->willReturn('M. Foo');
|
||||
|
||||
$owner = $this->getMockBuilder(Account::class)
|
||||
->disableOriginalConstructor()->getMock();
|
||||
$this->accountManager->expects($this->once())
|
||||
->method('getAccount')
|
||||
->with($user)
|
||||
->willReturn($owner);
|
||||
|
||||
$node->expects($this->once())
|
||||
->method('getDirectDownload')
|
||||
->willReturn(['url' => 'http://example.com/']);
|
||||
@@ -161,6 +174,18 @@ class FilesPluginTest extends TestCase {
|
||||
->method('getOwner')
|
||||
->willReturn($user);
|
||||
|
||||
$displayNameProp = $this->getMockBuilder(AccountProperty::class)
|
||||
->disableOriginalConstructor()->getMock();
|
||||
$owner
|
||||
->expects($this->once())
|
||||
->method('getProperty')
|
||||
->with(IAccountManager::PROPERTY_DISPLAYNAME)
|
||||
->willReturn($displayNameProp);
|
||||
$displayNameProp
|
||||
->expects($this->once())
|
||||
->method('getScope')
|
||||
->willReturn(IAccountManager::SCOPE_PUBLISHED);
|
||||
|
||||
$this->plugin->handleGetProperties(
|
||||
$propFind,
|
||||
$node
|
||||
@@ -179,6 +204,101 @@ class FilesPluginTest extends TestCase {
|
||||
$this->assertEquals([], $propFind->get404Properties());
|
||||
}
|
||||
|
||||
public function testGetDisplayNamePropertyWhenNotPublished(): void {
|
||||
/** @var File|\PHPUnit\Framework\MockObject\MockObject $node */
|
||||
$node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
|
||||
|
||||
$propFind = new PropFind(
|
||||
'/dummyPath',
|
||||
[
|
||||
FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME,
|
||||
],
|
||||
0
|
||||
);
|
||||
|
||||
$this->userSession->expects($this->once())
|
||||
->method('getUser')
|
||||
->willReturn(null);
|
||||
|
||||
$user = $this->getMockBuilder(User::class)
|
||||
->disableOriginalConstructor()->getMock();
|
||||
|
||||
$user
|
||||
->expects($this->never())
|
||||
->method('getDisplayName');
|
||||
|
||||
$owner = $this->getMockBuilder(Account::class)
|
||||
->disableOriginalConstructor()->getMock();
|
||||
$this->accountManager->expects($this->once())
|
||||
->method('getAccount')
|
||||
->with($user)
|
||||
->willReturn($owner);
|
||||
|
||||
$node->expects($this->once())
|
||||
->method('getOwner')
|
||||
->willReturn($user);
|
||||
|
||||
$displayNameProp = $this->getMockBuilder(AccountProperty::class)
|
||||
->disableOriginalConstructor()->getMock();
|
||||
$owner
|
||||
->expects($this->once())
|
||||
->method('getProperty')
|
||||
->with(IAccountManager::PROPERTY_DISPLAYNAME)
|
||||
->willReturn($displayNameProp);
|
||||
$displayNameProp
|
||||
->expects($this->once())
|
||||
->method('getScope')
|
||||
->willReturn(IAccountManager::SCOPE_PRIVATE);
|
||||
|
||||
$this->plugin->handleGetProperties(
|
||||
$propFind,
|
||||
$node
|
||||
);
|
||||
|
||||
$this->assertEquals(null, $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME));
|
||||
}
|
||||
|
||||
public function testGetDisplayNamePropertyWhenNotPublishedButLoggedIn(): void {
|
||||
/** @var File|\PHPUnit\Framework\MockObject\MockObject $node */
|
||||
$node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
|
||||
|
||||
$propFind = new PropFind(
|
||||
'/dummyPath',
|
||||
[
|
||||
FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME,
|
||||
],
|
||||
0
|
||||
);
|
||||
|
||||
$user = $this->getMockBuilder(User::class)
|
||||
->disableOriginalConstructor()->getMock();
|
||||
|
||||
$node->expects($this->once())
|
||||
->method('getOwner')
|
||||
->willReturn($user);
|
||||
|
||||
$loggedInUser = $this->getMockBuilder(User::class)
|
||||
->disableOriginalConstructor()->getMock();
|
||||
$this->userSession->expects($this->once())
|
||||
->method('getUser')
|
||||
->willReturn($loggedInUser);
|
||||
|
||||
$user
|
||||
->expects($this->once())
|
||||
->method('getDisplayName')
|
||||
->willReturn('M. Foo');
|
||||
|
||||
$this->accountManager->expects($this->never())
|
||||
->method('getAccount');
|
||||
|
||||
$this->plugin->handleGetProperties(
|
||||
$propFind,
|
||||
$node
|
||||
);
|
||||
|
||||
$this->assertEquals('M. Foo', $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME));
|
||||
}
|
||||
|
||||
public function testGetPropertiesStorageNotAvailable(): void {
|
||||
/** @var File|\PHPUnit\Framework\MockObject\MockObject $node */
|
||||
$node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
|
||||
@@ -215,6 +335,7 @@ class FilesPluginTest extends TestCase {
|
||||
$this->previewManager,
|
||||
$this->userSession,
|
||||
$this->filenameValidator,
|
||||
$this->accountManager,
|
||||
true,
|
||||
);
|
||||
$this->plugin->initialize($this->server);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user