Compare commits

..

297 Commits

Author SHA1 Message Date
Benjamin Gaussorgues c909f0125f feat(scan): add log for failed files scan
Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
2025-05-20 16:14:12 +02:00
Joas Schilling c62fa55007 Merge pull request #52851 from nextcloud/ci/noid/update-phpunit10
test: Prepare tests/ and some apps/*/tests/ for PHPUnit10
2025-05-15 09:30:33 +02:00
Joas Schilling 5f9117b939 test: Fix coding standards
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:48:13 +02:00
Joas Schilling 57a49a50aa test: Make extending tests also static already
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:29:33 +02:00
Joas Schilling ea0229dc86 test: Fix some apps/*/tests
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:26:15 +02:00
Joas Schilling 437f380fd9 test: Fix most tests/lib/DB/QueryBuilder
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:25:01 +02:00
Joas Schilling a316ae1590 fix: Fix warning when crash reporter fails to get loaded
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:25:01 +02:00
Joas Schilling 3e7db013c1 test: Fix non-static data providers
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:25:01 +02:00
Joas Schilling 9621e451ba test: Fix tests/lib/Files
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:25:00 +02:00
Joas Schilling e0a1c84ebb test: Make Archive test order independent
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:25:00 +02:00
Joas Schilling 2c74609e07 test: Fix tests/lib/[S-Z]*
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:24:59 +02:00
Joas Schilling f474b5b3a9 test: Fix tests/lib/Security/
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:24:59 +02:00
Joas Schilling c5cd7ef9b9 test: Fix tests/lib/[H-N]*
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:24:59 +02:00
Joas Schilling 3cc4410273 test: Fix tests/lib/[C-G]*
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:24:57 +02:00
Joas Schilling 720ab52e07 test: Fix tests/lib/App*
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:21:24 +02:00
Joas Schilling 5283e9a5e2 test: Cleanup tests/lib/Files/*
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:21:22 +02:00
Joas Schilling 53b116b8a5 test: Remove more withConsecutive
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-15 08:18:26 +02:00
F. E Noel Nfebe 158b3ef859 Merge pull request #52813 from nextcloud/fix/52794/share-advanced-settings
fix(files_sharing): Show note if note exists on share
2025-05-15 07:49:26 +02:00
Nextcloud bot fee878308e fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-15 00:22:57 +00:00
Kate 8ecadfa6bf Merge pull request #52820 from nextcloud/refactor/rector-core 2025-05-15 01:42:15 +02:00
Côme Chilliet 300964ec4b Merge pull request #52840 from nextcloud/fix/remove-broken-versions-routes
fix(files_versions): Remove routes leading to deleted ajax files
2025-05-15 00:19:30 +02:00
provokateurin 82fb8f8508 refactor: Extend rector to core/
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-15 00:16:54 +02:00
Kate c3ddd1da46 Merge pull request #52669 from nextcloud/chore/refactor-update-notification+ 2025-05-15 00:07:31 +02:00
Côme Chilliet baae99eaad Merge pull request #52667 from nextcloud/fix/improve-init-profiling
Improve init profiling
2025-05-14 22:57:55 +02:00
John Molakvoæ e65084adda Merge pull request #52841 from nextcloud/chore/dependabot 2025-05-14 22:47:20 +02:00
John Molakvoæ 970eb8299f Merge pull request #52359 from nextcloud/feat/mime-column 2025-05-14 22:42:27 +02:00
nextcloud-command 86c591ba5f chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-05-14 22:39:15 +02:00
nfebe 7810a9005e test(file_sharing): Do not open advanced settings manually
If a share note is present, the advanced settings should expand automatically,
without the user needing to click.

Signed-off-by: nfebe <fenn25.fn@gmail.com>
2025-05-14 22:39:15 +02:00
nfebe 63cf3f53c3 fix(files_sharing): Show note if note exists on share
When a share includes a note, ensure the note checkbox is checked and the note is
displayed when reopening the share sidebar.

Fixes: #52794.

Signed-off-by: nfebe <fenn25.fn@gmail.com>
2025-05-14 22:39:15 +02:00
Côme Chilliet 6974015e39 fix: Update psalm baseline to remove fixed issue
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-14 22:29:55 +02:00
Ferdinand Thiessen fd8df1003d chore: remove reviewers from dependendabot - its deprecated
Instead make sure all PRs get a reviewer assigned.
Also fixed an issue with the `dist/` folder as there is no automated PR
adding dist files - it needs to be manually updated, so that a PR
containing changes has to be reviewed properly!

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-14 22:27:25 +02:00
John Molakvoæ ae63945736 Merge pull request #52544 from nextcloud/chore/drop-deprecated-account-scopes 2025-05-14 22:15:01 +02:00
John Molakvoæ b3b63020c9 Merge pull request #52812 from nextcloud/chore/oc-helper-rmdirr 2025-05-14 21:53:47 +02:00
Côme Chilliet fd4c103e8b fix(files_versions): Remove routes leading to deleted ajax files
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-14 21:36:33 +02:00
nextcloud-command 8576c8baa6 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-05-14 17:53:32 +00:00
John Molakvoæ 7fc0318475 chore(files): adjust renaming action comment
Signed-off-by: John Molakvoæ <skjnldsv@users.noreply.github.com>
2025-05-14 17:51:12 +00:00
John Molakvoæ 006d4ee952 fix(files): hide mime column by default
Signed-off-by: John Molakvoæ <skjnldsv@users.noreply.github.com>
2025-05-14 17:51:12 +00:00
John Molakvoæ (skjnldsv) c7a50c708d fix(files): refresh Node data on extension change
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2025-05-14 17:51:12 +00:00
John Molakvoæ (skjnldsv) a4ab5a3cdf feat(files): allow showing file type (mime) column
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2025-05-14 17:51:12 +00:00
Benjamin Gaussorgues 1b72ddd8c8 Merge pull request #52684 from nextcloud/bugfix/noid/last-insert-id-when-reconnecting 2025-05-14 19:44:30 +02:00
Côme Chilliet 2cd491f491 Merge pull request #48560 from nextcloud/fix/migrate-encryption-away-from-hooks
feat(encryption): Migrate from hooks to events
2025-05-14 19:25:51 +02:00
Côme Chilliet 42f45030ff Merge pull request #52817 from nextcloud/chore/deps/rector-2.0
chore(deps): Update rector to ^2.0
2025-05-14 19:19:27 +02:00
Côme Chilliet 0c56605497 fix: Fix psalm issue and update baseline
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-14 19:18:31 +02:00
Ferdinand Thiessen 1b273b8c2c chore(IAccountManager): remove deprecated visibility constants
Those constants are not used anywhere anymore and are deprecated for
more than ten versions. So its time to cleanup the interface.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-14 19:15:24 +02:00
Andy Scherzinger 14f79829f3 Merge pull request #52775 from nextcloud/nested-jail-root
fix unjailedroot of nested jails if there are other wrappers in between
2025-05-14 17:36:55 +02:00
John Molakvoæ 4635cb4b2e Merge pull request #52809 from nextcloud/fix/preview-check 2025-05-14 15:50:01 +02:00
provokateurin 78a175fc74 refactor: Apply rector refactorings
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-14 15:29:02 +02:00
provokateurin 953d1ab962 chore(deps): Update rector to ^2.0
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-14 15:28:37 +02:00
provokateurin 2919685603 ci: Add vendor-bin/rector to dependabot
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-14 15:28:14 +02:00
Kate bfa494f247 Merge pull request #52800 from nextcloud/debt/noid/simple-file-generic-exception 2025-05-14 15:25:41 +02:00
Ferdinand Thiessen 072d4e8cba chore: move implementation from OC legacy to OCP\Files
- move implementation to the OCP variant that called the legacy before
- add the missing deprecation notice
- add missing parameter to align both signatures
- use OCP\Files where this method is still used

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-14 14:55:45 +02:00
Daniel 7523c38e58 Merge pull request #52798 from nextcloud/bug/noid/log-requests-exceeding-rate-limit
fix: log requests exceeding the rate limiting
2025-05-14 14:54:57 +02:00
Ferdinand Thiessen 1c7e4a1ba6 Merge pull request #52801 from nextcloud/chore/oc-helper-filesize
chore: replace legacy OC_Helper calls with OCP\Util
2025-05-14 14:37:13 +02:00
nextcloud-command 40239518c1 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-05-14 12:33:29 +00:00
John Molakvoæ (skjnldsv) 37d4bdce00 fix(files): do not even try to fetch a preview if èhas-preview` is false
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2025-05-14 14:22:01 +02:00
Ferdinand Thiessen a01e85799c Merge pull request #52802 from nextcloud/chore/oc-helper-can-exec
chore: deprecate \OC_Helper::canExecute
2025-05-14 12:45:25 +02:00
Daniel Kesselberg a53e15c971 fix: log requests exceeding the rate limiting
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
2025-05-14 12:23:40 +02:00
Joas Schilling 7304756e03 test: Sort the reactions before comparing
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-14 11:42:52 +02:00
Robin Appelman 1950076ca1 Merge pull request #52378 from nextcloud/transfer-external-storage
feat: add option to include external storage when transfering files
2025-05-14 11:10:32 +02:00
Daniel 498c57f648 Merge pull request #52046 from nextcloud/bug/49834/calendar-unsharing
Introduce own method for calendar unsharing
2025-05-14 10:59:59 +02:00
John Molakvoæ 3aae7ae305 Merge pull request #52792 from nextcloud/fix/mime-fallback-public 2025-05-14 10:51:40 +02:00
Joas Schilling df94cceb7b fix(db): Store last insert id before reconnect
During a reconnect we are losing the connection and when the
realLastInsertId call is the one triggering the reconnect, it
does not return the ID. But inside the reconnect, we were able
to save the last insert id, so calling it a second time is going
to be successful.
We can not return the result on the initial call, as we are already
way deeper in the stack performing the actual database query on
the doctrine driver.

Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-14 10:27:00 +02:00
Robin Appelman b67a43764a Merge pull request #52694 from nextcloud/fixHardcodedVersionsFolder
fix(files_versions): Folder should not be hardcoded
2025-05-14 10:18:07 +02:00
Ferdinand Thiessen 1ae3fa4003 chore: replace leagcy OC_Helper calls with OCP\Util
- Replace legacy calls with OCP\Util
- Add missing deprecation notices
- Inline implementation in OCP\Util and call it from OC_Helper

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-14 10:18:04 +02:00
Kate 0f03a892b9 Merge pull request #52782 from nextcloud/feat/log-client-side-req-id 2025-05-14 10:07:36 +02:00
Ferdinand Thiessen 85a6b0d0eb chore: deprecate \OC_Helper::canExecute
replace this legacy method with just the IBinaryFinder

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-14 09:55:14 +02:00
Daniel Kesselberg 023b98c44b fix(dav): only consider user's principal for unsharing entries
Before: Find all entries in `dav_shares` with `access = 5` for the user's principal, as well as group and circle memberships.

After: Find all entries in `dav_shares` with `access = 5` solely for the user's principal.

Future support for unsharing group or circle principals could be considered as a feature enhancement.

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
2025-05-14 09:03:33 +02:00
Daniel Kesselberg c05d3fdb2e fix(caldav): prevent unshare entry creation for owner unsharing
- Introduces a `unshare` method in `CalDavBackend` to handle user unshares.
- Implements check to determine if unshare entry is needed based on group/circle membership.
- Ensures `updateShares` is only used when the calendar owner manages shares.
- Resolves issue where unsharing a calendar as owner created an unshare entry in `oc_dav_shares`.

Related PRs:
- https://github.com/nextcloud/server/pull/43117
- https://github.com/nextcloud/server/pull/47737

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
2025-05-14 09:03:32 +02:00
Nextcloud bot a338772ddd fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-14 00:22:28 +00:00
Côme Chilliet 2b4bfcc212 fix(log): Only log client request id if present, and at the end
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-14 00:35:21 +02:00
Côme Chilliet 4c25654574 feat(log): Log the request id sent with the request
This should help matching client logs with server logs, hopefully.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-14 00:35:21 +02:00
Côme Chilliet 43418eea5f fix(tests): Set encryption configuration even earlier so that all users are created with private key
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet a86d917907 fix(encryption): Only prevent cache deletion if target is not object store in moveFromStorage
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet a79b5dea7c fix(encryption): Improve Update class and event listenening
to avoid back&forth between path and Node object

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 2d8f6b366a chore: Assert rename success in versionning tests
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet e6275f8759 fix: Preserve file id when moving from object store even if encryption wrapper is present
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 27599ef45d fix(encryption): Fix a PHP error in Encryption Util in specific situations
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 08bff4cf4a fix(admin_audit): Survive if file change id after rename (it should not)
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 367c877b7a fix(tests): Avoid user login before a private key is setup
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet f6f83430a9 chore: Update psalm baseline to remove fixed issue
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 561b590c77 chore(trashbin): Fix configuration mocking in trashbin tests
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 14872c8040 chore(files_versions): Only mock getSystemValue method to avoid problems in files_versions tests
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet e35a8ed063 fix(tests): Disable encryption wrapper when it makes sense
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 9bb0721d66 fix: Fix mtime preservation when moving a directory across storages with encryption registered
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 8779ae38a4 fix(encryption): Fix filesize for part files in Encryption wrapper
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 21233b7e17 fix(tests): Remove Encryption disabling in ViewTest to avoid side effects
Adapt tests a bit to make them pass with Encryption wrapper registered

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 38f341c179 fix(tests): Unregister encryption modules in ViewTest to avoid side effects
It was clearing the hooks with the same results before

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet 536ccf144c feat(encryption): Migrate from hooks to events
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 23:37:52 +02:00
Côme Chilliet fb615ef9f8 Merge pull request #52688 from nextcloud/feat/ocp-sanitize-filenames
feat(FilenameValidator): allow to sanitize filenames
2025-05-13 23:35:43 +02:00
Daniel Kesselberg 866063c198 fix: annotate possible exceptions
File.getContent can throw a GenericFileException since https://github.com/nextcloud/server/pull/37943.

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
2025-05-13 22:27:19 +02:00
Christoph Wurst 8c68f1eeb7 Merge pull request #52778 from nextcloud/fix/noid-check-for-properties-before-processing
fix: check if properties exist before using them
2025-05-13 18:58:16 +02:00
Robin Appelman 1e72620169 fix: fix unjailedroot of nested jails if there are other wrappers in between
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-13 18:29:00 +02:00
Robin Appelman 0001c22668 test: add test for nested cache jail unjailedroot
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-13 18:29:00 +02:00
John Molakvoæ (skjnldsv) cd6a53ea4d fix(files_sharing): add mime fallback to public preview too
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2025-05-13 18:28:54 +02:00
John Molakvoæ b9da9416be Merge pull request #52785 from nextcloud/feat/file-drop-recursive 2025-05-13 18:27:19 +02:00
Côme Chilliet 20c6d1a7e9 feat: Improve init a bit, and add more profiling steps
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-05-13 16:08:49 +02:00
John Molakvoæ (skjnldsv) b286bca485 fix(dav): remove unnecessary plugin getHTTPMethods
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2025-05-13 16:03:20 +02:00
John Molakvoæ (skjnldsv) 4495794a0b feat(dav): allow uploading folders to public shares
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2025-05-13 16:03:20 +02:00
Ferdinand Thiessen 6cf1870322 feat(FilenameValidator): allow to sanitize filenames
Share the filename sanitizing with the OCP filename validator.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-13 14:14:17 +02:00
Robin Appelman 56897b6f3c Merge pull request #52681 from nextcloud/occ-external-dependencies
feat: add command to check files_external dependencies
2025-05-13 11:13:24 +02:00
Ferdinand Thiessen 76696be762 chore: remove legacy migration step to remove background job
This is already included since Nextcloud 29, so the background job is
removed and the class does not exist anymore.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-13 10:04:35 +02:00
Ferdinand Thiessen 9255afaeab refactor(updatenotification): use consistant patterns for on-demand class members
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-13 10:04:35 +02:00
Ferdinand Thiessen be3cf85656 refactor(updatenotification): use constant instead of variable
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-13 10:04:35 +02:00
Ferdinand Thiessen 6e9b4eb0a8 refactor(updatenotification): add return types and fix typos
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-13 10:04:35 +02:00
Ferdinand Thiessen ff318138a2 refactor(updatenotification): use OCP\ServerVersion
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-13 10:00:34 +02:00
Ferdinand Thiessen 1a8a6bea97 refactor: use APP_NAME where possible
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-13 09:49:28 +02:00
Robin Appelman f97495bf5a feat: add command to check files_external dependencies
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-13 09:46:19 +02:00
Côme Chilliet 2f1c74d43f Merge pull request #51920 from nextcloud/newfolder-race-improvements
fix: improve handling of newFolder race condition handling
2025-05-13 09:16:54 +02:00
Nextcloud bot d4b38083d6 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-13 00:22:59 +00:00
Ferdinand Thiessen d0e9e2c859 Merge pull request #52771 from nextcloud/fix/update-notification-respect-config
fix(updatenotification): respect `updatechecker` config
2025-05-12 23:30:12 +02:00
SebastianKrupinski ebd80bc142 fix: check if properties exist before using them
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
2025-05-12 15:44:07 -04:00
John Molakvoæ 0eb530ecde Merge pull request #52774 from nextcloud/feat/oracle-setup-cypres 2025-05-12 19:41:31 +02:00
Ferdinand Thiessen 957efe0670 fix(updatenotification): respect updatechecker config
If disabled:
- Hide admin settings
- Do not create Nextcloud server update notifications

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-12 18:06:41 +02:00
John Molakvoæ (skjnldsv) 3500018b56 feat(cypress): add oracle to setup tests
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2025-05-12 17:50:17 +02:00
Louis a2eed985aa Merge pull request #52686 from nextcloud/isNumericMtime 2025-05-12 13:21:51 +02:00
Robin Appelman 7e9fc7604a Merge pull request #52693 from nextcloud/config-system-set-json
feat: allow setting system config values with json value
2025-05-12 13:18:26 +02:00
Robin Appelman 768b101a9d Merge pull request #52706 from nextcloud/info-file-more-encryption-checks
feat: add more encryption checks to info:file
2025-05-12 13:17:26 +02:00
Robin Appelman e6bdfcd8ed Merge pull request #52665 from nextcloud/mountpoint-mkdir-quota
fix: create mountpoint folder even if the user has a quota of 0
2025-05-12 13:17:02 +02:00
Robin Appelman ffe10d4916 feat: allow setting system config values with json value
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-12 12:02:40 +02:00
John Molakvoæ ff1dfc6bd0 Merge pull request #52703 from nextcloud/feat/setup-checks
chore(cypress): add setup tests
2025-05-12 11:05:04 +02:00
Nextcloud bot 0827645b53 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-12 00:22:47 +00:00
Ferdinand Thiessen a4d7c12326 Merge pull request #52747 from nextcloud/rakekniven-patch-1
chore(i18n): Adapted spelling of product name "Windows"
2025-05-11 11:32:30 +02:00
rakekniven ca1f779258 chore(i18n): Adapted spelling of product name "Windows"
Reported at Transifex.

Signed-off-by: rakekniven <2069590+rakekniven@users.noreply.github.com>
2025-05-11 10:26:16 +02:00
Nextcloud bot 195dbad119 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-11 00:22:42 +00:00
Kate 0a73113b35 Merge pull request #52707 from nextcloud/encryption-no-header-size-error 2025-05-10 09:47:21 +02:00
dependabot[bot] a4c923f4b9 Merge pull request #52712 from nextcloud/dependabot/composer/build/integration/behat/behat-3.22.0 2025-05-10 03:33:55 +00:00
dependabot[bot] 25e7c10a33 Merge pull request #52725 from nextcloud/dependabot/npm_and_yarn/vitest/coverage-v8-3.1.3 2025-05-10 03:33:50 +00:00
dependabot[bot] 0c200fb9ef build(deps-dev): bump @vitest/coverage-v8 from 3.1.2 to 3.1.3
Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.1.3/packages/coverage-v8)

---
updated-dependencies:
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 3.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-10 01:57:30 +00:00
dependabot[bot] 70db2a4c9e build(deps-dev): bump behat/behat in /build/integration
Bumps [behat/behat](https://github.com/Behat/Behat) from 3.21.1 to 3.22.0.
- [Release notes](https://github.com/Behat/Behat/releases)
- [Changelog](https://github.com/Behat/Behat/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Behat/Behat/compare/v3.21.1...v3.22.0)

---
updated-dependencies:
- dependency-name: behat/behat
  dependency-version: 3.22.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-10 01:22:38 +00:00
Nextcloud bot 2624d2b702 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-10 00:22:59 +00:00
Robin Appelman b0b8159d6a fix: throw a better error if we can't get the encrypted header size
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-09 22:33:45 +02:00
skjnldsv 3dbc479239 chore(cypress): add setup tests
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
2025-05-09 21:59:23 +02:00
John Molakvoæ d7f8a106cc Merge pull request #52690 from nextcloud/Valdnet-patch-1 2025-05-09 21:21:25 +02:00
Robin Appelman d9c53ef748 feat: add more encryption checks to info:file
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-09 16:35:31 +02:00
Sebastian Krupinski 0b354efb22 Merge pull request #51081 from nextcloud/fix/noid/calendar-enabled
fix(CalDAV): add calendar enable
2025-05-09 15:21:59 +01:00
SebastianKrupinski 477bc4e3f1 fix: add calendar enable
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
2025-05-09 09:05:21 -04:00
Alexander Piskun c7430d5cb8 Merge pull request #52699 from nextcloud/fix/noid/AppAPI-invalid-userId
fix(webhooks_listener): correctly set userId from event
2025-05-09 13:37:01 +03:00
Oleksander Piskun aa0135c223 fix(webhooks_listener): correctly set userId from event
Signed-off-by: Oleksander Piskun <oleksandr2088@icloud.com>
2025-05-09 13:07:33 +03:00
Git'Fellow 1b549d4943 fix(files_versions): Folder should not be hardcoed
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2025-05-09 09:35:17 +02:00
nextcloud-command 5435f3bab0 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-05-09 07:04:07 +00:00
rakekniven 2ed16599e1 chore(i18n): Use plural
Signed-off-by: rakekniven <2069590+rakekniven@users.noreply.github.com>
2025-05-09 07:01:56 +00:00
Valdnet 95af3305d8 Spelling standardization
Signed-off-by: Valdnet <47037905+Valdnet@users.noreply.github.com>
2025-05-09 07:01:56 +00:00
Nextcloud bot 04b1dfdf13 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-09 00:23:45 +00:00
Kate 648745d524 Merge pull request #52634 from nextcloud/perf/share20/get-all-shares-in-folder 2025-05-08 16:24:21 +02:00
provokateurin ae16a28758 perf(Share20): Add interface to query all shares in a folder without filtering by user
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-08 15:29:40 +02:00
Robin Appelman 99364adc1c fix: improve handling of newFolder race condition handling
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-08 15:19:40 +02:00
Robin Appelman cd75876c1e fix: create mountpoint folder even if the user has a quota of 0
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-08 15:15:36 +02:00
Robin Appelman 1228cfd3a2 Merge pull request #52133 from nextcloud/no-shared-direct-download
fix: disable direct download for shares
2025-05-08 15:14:11 +02:00
Git'Fellow 2d68644e11 fix(files_external): Safely check if the timestamp is numeric
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2025-05-08 11:46:52 +02:00
Kate 349cc7b732 Merge pull request #52543 from nextcloud/fix/52060/manage-download-on-federated-reshare 2025-05-08 08:07:45 +02:00
Nextcloud bot 7c6f8d2a25 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-08 00:23:25 +00:00
Robin Appelman 3a02d942ae fix: handle transfering the root of a mountpoint
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-07 18:31:15 +02:00
Robin Appelman 2c0e576a7d feat: add option to include external storage when transfering files
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-05-07 18:31:13 +02:00
John Molakvoæ 0dc971189b Merge pull request #52636 from nextcloud/test/files-download 2025-05-07 11:16:17 +02:00
John Molakvoæ e3e6a3b72e Merge pull request #52438 from nextcloud/fix/middle-click 2025-05-07 10:53:12 +02:00
nextcloud-command a5b7af3d74 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-05-07 08:33:36 +00:00
skjnldsv c446274bf0 fix(files): middle click & ctrl new tab
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
2025-05-07 10:29:36 +02:00
Kate 7880a5c25e Merge pull request #52375 from nextcloud/fix/noid/metadata-on-fresh-setup 2025-05-07 09:40:30 +02:00
Maxence Lange cd06b2432c Merge pull request #52623 from nextcloud/feat/noid/get-value-type-from-lexicon
iAppConfig: getValueType() get data from lexicon if available
2025-05-07 00:04:46 -01:00
Nextcloud bot 8528916bbc fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-07 00:23:10 +00:00
Daniel 1c2b4f2a41 Merge pull request #52586 from nextcloud/bugfix/noid/remove-sleep-from-throttler
fix(throttler): Always use the sleepDelayOrThrowOnMax instead of deprecated sleepDelay
2025-05-06 19:22:53 +02:00
Maxence Lange cc96782177 fix(share): assume download enabled on federated share
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
2025-05-06 14:48:28 -01:00
F. E Noel Nfebe 598579396c Merge pull request #52511 from nextcloud/feat/no-issue/show-remote-shares-as-internal-config
feat(files_sharing): Add command to control display area for federated shares
2025-05-06 16:20:17 +02:00
nextcloud-command 46b98f3bcc chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-05-06 13:32:45 +00:00
nfebe f471bd292f fix(files_sharing): Move ConfigLexicon to Config dir, update sharing input
- Fix autoloading for new `ConfigLexicon`
- Ensure that sharing input in sharing tab respect `show-federated-shares-as-internal`:
This is important, because when federated shares are shown as internal the users should add them from the internal shares section

Signed-off-by: nfebe <fenn25.fn@gmail.com>
2025-05-06 14:28:01 +01:00
Maxence Lange 4c241d4f4b refactor: use config lexicon
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
2025-05-06 14:28:01 +01:00
nfebe 1c50329fdb feat(files_sharing): Add toggle for federated share display area
Signed-off-by: nfebe <fenn25.fn@gmail.com>
2025-05-06 14:28:01 +01:00
Kate 87bad33496 Merge pull request #52047 from nextcloud/sub-mount-filter-no-storage 2025-05-06 15:25:23 +02:00
Kate 206aac5b5b Merge pull request #52128 from nextcloud/shared-target-verify-cache-fix 2025-05-06 15:25:09 +02:00
Kate c1a9dfd993 Merge pull request #52441 from nextcloud/scan-locked-error 2025-05-06 15:23:16 +02:00
Kate 9592390070 Merge pull request #51779 from nextcloud/object-store-filename 2025-05-06 15:21:01 +02:00
John Molakvoæ 24c6a1ff0b Merge pull request #52182 from nextcloud/feat/dav/public-share-chunked-upload 2025-05-06 14:25:28 +02:00
Louis 1c518a291d Merge pull request #52360 from nextcloud/artonge/fix/use_preview_api_for_blurhash_generation 2025-05-06 14:12:50 +02:00
Daniel 31899d95b9 Merge pull request #51924 from nextcloud/feat/issue-563-calendar-export
feat: Calendar Export
2025-05-06 13:05:20 +02:00
Kate 5cf799bdb4 Merge pull request #52642 from nextcloud/fix/task-cleanup-delay 2025-05-06 11:56:42 +02:00
Kent Delante 7c4098d8c7 Merge pull request #52583 from nextcloud/leftybournes/fix/app-sorting
fix(apps): Sort names separately from active/update state
2025-05-06 17:27:59 +08:00
nextcloud-command 7896575af8 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-05-06 09:09:42 +00:00
SebastianKrupinski a2d4f8d3f1 feat: Calendar Export
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
2025-05-06 11:09:33 +02:00
Kate cd9f0350b0 Merge pull request #52621 from nextcloud/feat/noid/files-external-lexicon 2025-05-06 10:27:28 +02:00
Nextcloud bot 25c34ce1e5 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-06 00:23:26 +00:00
Edward Ly 6af7ca0a85 fix(TaskProcessing): increase task cleanup delay
Signed-off-by: Edward Ly <contact@edward.ly>
2025-05-05 11:24:13 -07:00
Maxence Lange 6bd5f6af83 feat(files_external): support lexicon
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
2025-05-05 17:05:45 -01:00
Arthur Schiwon 2a1e63be14 Merge pull request #52048 from nextcloud/fix/noid/wfe-empty-group-in-check
fix(workflowengine): fix group not shown in Group membership check
2025-05-05 17:34:36 +02:00
Arthur Schiwon c051a5b650 build: compile assets
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2025-05-05 17:01:04 +02:00
Arthur Schiwon c2dea5faf6 fix(workflowengine): fix group not shown in Group membership check
this might have occured on instances with
- more than twenty groups, and
- on rules with more than one Group membership checks
- and at least one of them being not in the set of the first 20 groups

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2025-05-05 16:58:16 +02:00
Ferdinand Thiessen 88829b3abf test: add e2e tests for multiple-files download
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-05-05 16:03:08 +02:00
Ferdinand Thiessen c668703021 Merge pull request #51608 from nextcloud/feat/sanitize-filenames-command
feat(files): add command to automatically rename filenames
2025-05-05 15:47:30 +02:00
Kate 0e0abcbdc4 Merge pull request #52630 from nextcloud/fix/files/activity-rich-object-strings 2025-05-05 15:30:54 +02:00
provokateurin 01a8d7748c chore(deps): Update @nextcloud/upload to ^1.10.0
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-05 15:27:12 +02:00
Kate 66c2607365 Merge pull request #52628 from nextcloud/fix/files_external/hidden-password-fields 2025-05-05 14:33:37 +02:00
provokateurin cfdb919c37 fix(files): Fix non-string values for activity rich object strings
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-05 14:05:35 +02:00
provokateurin 46f5b07322 feat(dav): Enable chunked upload for public shares
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-05 13:23:11 +02:00
provokateurin a55e61d97c feat(dav): Allow UploadHome to handle public shares
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-05 13:23:11 +02:00
provokateurin 8813df9623 refactor(dav): Pass UID from UploadHome to UploadFolder and CleanupService
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-05 13:23:11 +02:00
provokateurin 7f0953d520 refactor(dav): Replace baseuri manipulation with RootCollection for public shares
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-05 13:23:11 +02:00
provokateurin e90e3a70fa feat(dav): Allow share principals
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-05 13:23:10 +02:00
provokateurin db5f6a4e96 fix(files_external): Mark password fields for LoginCredentials and SessionCredentials as hidden and optional
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-05 13:19:48 +02:00
provokateurin 01d2a9f76a fix(files_external): Convert VALUE_HIDDEN to FLAG_HIDDEN to allow combining VALUE_PASSWORD and FLAG_HIDDEN
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-05-05 13:19:43 +02:00
Maxence Lange b3f52ebbe8 feat(appconfig): getValueType() get data from lexicon if available
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
2025-05-05 10:17:02 -01:00
Richard Steinmetz 4783459144 Merge pull request #52589 from nextcloud/fix/dav/orphan-cleanup-job
fix(dav): move orphan cleaning logic to a chunked background job
2025-05-05 12:32:14 +02:00
Andy Scherzinger 5644f33b44 Merge pull request #52581 from spalmurray/master
ci: pin Codecov action versions by hash
2025-05-05 12:20:36 +02:00
Richard Steinmetz d0fcb7e638 fix(dav): move orphan cleaning logic to a chunked background job
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
2025-05-05 12:13:09 +02:00
Louis Chemineau 867be352f3 fix(blurhash): Use preview API to generate the previews
This allows to benefit from all the checks done by the preview API.
This also use the newly introduced `cacheResult` argument to limit disk usage.

Signed-off-by: Louis Chemineau <louis@chmn.me>
2025-05-05 11:13:31 +02:00
Joas Schilling e6bcc4e4e9 Merge pull request #52626 from nextcloud/bugfix/noid/make-comments-test-better-readable
test(comments): Make Comments test result output properly readable
2025-05-05 11:03:04 +02:00
Louis Chemineau 2238548278 feat(previews): Support in memory preview request
This allows callers to use the API without increasing the disk usage.

Example: blurhash generation, where we request a preview for all uploaded pictures, but don't want to necessarily store that preview.
Signed-off-by: Louis Chemineau <louis@chmn.me>
2025-05-05 10:38:51 +02:00
Joas Schilling d46d2d1acf test: Fix coding standards
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-05 10:09:22 +02:00
Joas Schilling 10424f248b test(comments): Make Comments test result output properly readable
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-05 09:38:02 +02:00
Kate ed485a9731 Merge pull request #52587 from nextcloud/bugfix/noid/fix-type-error 2025-05-05 09:08:56 +02:00
Joas Schilling aba39c717a Merge pull request #52588 from nextcloud/test/noid/more-phpunit-10
test: Finish migrating tests/Core/ to PHPUnit 10 compatible code
2025-05-05 09:08:40 +02:00
Nextcloud bot ca1fc249ce fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-05 00:49:13 +00:00
Simon L. f068350998 Merge pull request #52584 from nextcloud/enh/noid/fix-docs-ci
fix(CI): fix CI in documentation repository
2025-05-04 13:38:05 +02:00
Simon L. 8683a9bb4c fix(CI): fix CI in documentation repository
Signed-off-by: Simon L. <szaimen@e.mail.de>
2025-05-04 13:10:17 +02:00
Nextcloud bot e167a567f2 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-04 00:22:42 +00:00
Nextcloud bot d99c797aac fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-03 00:22:42 +00:00
Joas Schilling c24f5fb256 test: Finish migrating tests/Core/ to PHPUnit 10 compatible code
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-02 16:47:42 +02:00
Joas Schilling 9009ef1a4c fix(loginflow): Fix type error when password could not be decrypted
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-02 14:08:50 +02:00
Joas Schilling 7964f338dc fix(throttler): Remove the sleep from the throttler that throws
The sleep is not adding benefit when it's being aborted with 429
in other cases anyway.

Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-05-02 11:27:29 +02:00
Andy Scherzinger 9a16e4fd14 Merge pull request #52542 from nextcloud/artonge/fix/forward_extra
fix: Forward 'extra' argument when optimizing query
2025-05-02 08:48:58 +02:00
Kent Delante 9fbb3d8b4d fix(apps): Sort names separately from active/update state
Signed-off-by: Kent Delante <kent.delante@proton.me>
2025-05-02 14:10:38 +08:00
Nextcloud bot b231e18867 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-02 00:22:57 +00:00
Spencer Murray ecacdd2e77 ci: pin codecov/test-results-action@v1.1.0 with hash
Signed-off-by: Spencer Murray <spencer.murray@sentry.io>
2025-05-01 11:39:23 -04:00
Spencer Murray dbfbb8da7f ci: pin codecov/codecov-action@v5.4.2 with hash
Signed-off-by: Spencer Murray <spencer.murray@sentry.io>
2025-05-01 11:39:22 -04:00
Spencer Murray 4fe5c8a217 ci: do not upload codecov bundle when TESTING=true
Signed-off-by: Spencer Murray <spencer.murray@sentry.io>
2025-05-01 11:39:14 -04:00
John Molakvoæ 8b07a9be20 Merge pull request #52572 from nextcloud/fix/noid/flaky-sso-tests 2025-05-01 16:13:15 +02:00
Andy Scherzinger d692f39f9f Merge pull request #52575 from nextcloud/rakekniven-patch-1
fix(notification): Notification in activity shows "Rich subject or a …
2025-05-01 13:16:07 +02:00
Marcel Müller 99d23a434c fix: Ensure samba is started in kerberos tests
Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
2025-05-01 11:15:48 +02:00
Marcel Müller 3cf7b8ddbe chore: Add logs for dc and apache container
Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
2025-05-01 11:15:24 +02:00
rakekniven 1d80bcb97c fix(notification): Notification in activity shows "Rich subject or a parameter for ..."
Signed-off-by: rakekniven <2069590+rakekniven@users.noreply.github.com>
2025-05-01 08:59:46 +02:00
Nextcloud bot 112f6f43fb fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-01 00:23:00 +00:00
dependabot[bot] 8788d659c8 Merge pull request #52571 from nextcloud/dependabot/npm_and_yarn/vite-6.3.4 2025-04-30 20:29:22 +00:00
Daniel be1982568a Merge pull request #52546 from nextcloud/fix/52278/remove-unused-etag-check
fix: Remove unneccesary etag check
2025-04-30 22:28:27 +02:00
Marcel Müller ddd91793bc fix: Add etag tests to NavigationControllerTest
Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
2025-04-30 21:48:34 +02:00
dependabot[bot] 3eb797ddf7 build(deps): bump vite from 6.2.6 to 6.3.4
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.6 to 6.3.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-30 19:38:01 +00:00
Louis a67720552e Merge pull request #52551 from nextcloud/checkResultArray
fix(WeatherStatus): Check if result is an array
2025-04-30 21:30:03 +02:00
Ferdinand Thiessen df69eb56d1 Merge pull request #52269 from nextcloud/fix/files-summary
fix(files): make sure to always have proper plural forms
2025-04-30 19:11:47 +02:00
Ferdinand Thiessen 2036beec06 Merge pull request #52565 from nextcloud/artonge/fix/attribute_in_sidebar
fix: Forward attributes to the sidebar
2025-04-30 18:07:08 +02:00
nextcloud-command 0fb8daa7bb chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-04-30 15:59:19 +00:00
Ferdinand Thiessen 10d17de5bc Merge pull request #52298 from nextcloud/test/no-git-ignore
test: ignore git-ignored files from tests
2025-04-30 17:55:22 +02:00
Ferdinand Thiessen 04f7d4a100 fix(files): make sure to always have proper plural forms
counted words should always be translated using `n`.
We can still simply concat the segments with punctation, this always
works regardless of the language used (even with RTL / LTR languages).

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-04-30 17:54:54 +02:00
Louis Chemineau eab2d9e3a9 fix: Forward attributes to the sidebar
Needed for Photos' sidebar tab

Signed-off-by: Louis Chemineau <louis@chmn.me>
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-04-30 15:27:39 +00:00
Ferdinand Thiessen 3657c2a650 docs(files): add more documentation about WCF and links to the admin docs
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-04-30 16:32:49 +02:00
Ferdinand Thiessen f7f4930581 feat(files): add command to (dis)enable windows compatible filenames
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-04-30 16:32:49 +02:00
Ferdinand Thiessen 226ad23a1a feat(files): add command to automatically rename filenames
Co-authored-by: Ferdinand Thiessen <opensource@fthiessen.de>
Co-authored-by: Côme Chilliet <91878298+come-nc@users.noreply.github.com>
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-04-30 16:32:07 +02:00
Christoph Wurst 535253e0d2 Merge pull request #52228 from nextcloud/fix/issue-50748-calendar-object-move
fix(CalDAV): do not ignore move command object target uri
2025-04-30 16:27:48 +02:00
Daniel 391e09331f Merge pull request #52558 from nextcloud/chore/constants/sort
chore(Constants): Sort by value
2025-04-30 16:22:01 +02:00
provokateurin 6c127e2fdd chore(Constants): Sort by value
Signed-off-by: provokateurin <kate@provokateurin.de>
2025-04-30 14:20:12 +02:00
John Molakvoæ 57e0ffcb27 Merge pull request #52532 from nextcloud/fix/public-displayname-owner 2025-04-30 11:32:05 +02:00
Joas Schilling d20e4d2fe9 Merge pull request #52552 from nextcloud/techdebt/noid/more-phpunit-10-preparations
tests: Prepare more tests for PHPUnit 10
2025-04-30 09:42:57 +02:00
Joas Schilling b77011a918 test: Prepare more tests for PHPUnit 10
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-04-30 09:18:01 +02:00
John Molakvoæ c06d851ead Merge pull request #52535 from nextcloud/fix/public-owner-scope 2025-04-30 08:59:22 +02:00
nextcloud-command b95695b110 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-04-30 06:43:23 +00:00
skjnldsv 58aaddeca5 fix(dav): check the owner displayName scope before giving attribute
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
2025-04-30 08:37:08 +02:00
Git'Fellow 9c8e026003 fix (WeatherStatus): Check if result is an array
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2025-04-30 08:01:35 +02:00
Nextcloud bot 88aa80e847 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-04-30 00:23:19 +00:00
Marcel Müller 1addd35b78 fix: Remove unneccesary etag check
Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
2025-04-29 23:15:50 +02:00
Richard Steinmetz 6f661deb75 Merge pull request #52534 from nextcloud/perf/caldav/bigger-chunks-orphan-repair
perf(caldav): increase chunk size in RemoveOrphanEventsAndContacts repair step
2025-04-29 23:05:05 +02:00
skjnldsv bcc8de0c9d fix(files_sharing): bring back owner and ownerDisplayName initial state
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
2025-04-29 21:25:47 +02:00
Louis Chemineau 5c73f2c37f fix: Forward 'extra' argument when optimizing query
This allows DAV SEARCH queries containing optimizable comparisons on files metadata like:

```xml
				<d:or>
					<d:eq>
						<d:prop>
							<nc:metadata-photos-place />
						</d:prop>
						<d:literal>La Valette-du-Var</d:literal>
					</d:eq>
					<d:eq>
						<d:prop>
							<nc:metadata-photos-place />
						</d:prop>
						<d:literal>Évenos</d:literal>
					</d:eq>
				</d:or>
```

Signed-off-by: Louis Chemineau <louis@chmn.me>
2025-04-29 18:17:23 +02:00
Andy Scherzinger f26dc79480 Merge pull request #52179 from spalmurray/master
ci: add Codecov's test analytics and bundle analysis features
2025-04-29 17:27:36 +02:00
Richard Steinmetz 33a8e2226c perf(caldav): increase chunk size in RemoveOrphanEventsAndContacts repair step
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
2025-04-29 16:34:19 +02:00
Joas Schilling e7fbfa61e5 Merge pull request #52051 from Lukasdotcom/feat/text-to-speech
feat(TaskProcessing): Add TextToSpeech provider
2025-04-29 15:41:21 +02:00
F. E Noel Nfebe 008e9f2101 Merge pull request #52526 from nextcloud/fix/no-issue/prevent-create-delete-perms-on-file-shares
fix(files_sharing): Prevent create/delete permissions on file shares
2025-04-29 14:14:14 +02:00
nextcloud-command 9483507093 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2025-04-29 11:49:59 +00:00
Ferdinand Thiessen 28df049b99 Merge pull request #52525 from nextcloud/fix/noid/wfe-set-inital-value
fix(WFE): properly set inital status
2025-04-29 13:48:04 +02:00
nfebe 99ae04d9ce fix(files_sharing): Prevent create/delete permissions on file shares
File shares can't support create or delete permissions. This change ensures those permissions are
 stripped or ignored when the shared item is not a folder.

Signed-off-by: nfebe <fenn25.fn@gmail.com>
2025-04-29 13:42:02 +02:00
Ferdinand Thiessen 698146c366 Merge pull request #51592 from nextcloud/fix/search-cast
fix(dav): throw invalid argument when property type does not match
2025-04-29 13:41:48 +02:00
Arthur Schiwon 94202f2664 build: compile assets
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2025-04-29 12:23:36 +02:00
Arthur Schiwon 09bb0720d4 fix(WFE): properly set inital status
The operationComponent.value is only possibly available with registered
operations that have the value prop. Ones that do not would not have the
value set properly initially. Going to inputValue, which is mapped to the
components model-value, solves this.

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2025-04-29 12:23:27 +02:00
Ferdinand Thiessen 9df998e710 Merge pull request #52369 from nextcloud/fix/files-versions-author
fix(files_versions): wait for version to be created before setting metadata
2025-04-29 11:01:58 +02:00
Ferdinand Thiessen 0e50ba839c test: ignore git-ignored files from tests
Fix running `npm run test` when your development setup has other apps
checked out - like having the viewer app installed locally in `apps`.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-04-29 10:15:29 +02:00
Stephan Orbaugh f9b6b88d5c Merge pull request #52479 from RobertZenz/bugfix/fix-jsresourcelocator-incorrect-parameter
fix(JSResourceLocator) fix incorrect parameter being used.
2025-04-29 09:10:51 +02:00
Nextcloud bot 0d98f91ae6 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-04-29 00:22:42 +00:00
Spencer Murray 0395eaacbb ci: change if always() to if not cancelled on test results uploads
Signed-off-by: Spencer Murray <spencer.murray@sentry.io>
2025-04-28 16:01:50 -04:00
Spencer Murray 9c3bdf2647 ci: always upload test results, even if tests fail
Signed-off-by: Spencer Murray <spencer.murray@sentry.io>
2025-04-28 16:01:49 -04:00
Spencer Murray c795fb1937 ci: fix whitespace
Signed-off-by: Spencer Murray <spencer.murray@sentry.io>
2025-04-28 16:01:49 -04:00
Spencer Murray cb4e76060d ci: tweak Codecov webpack plugin config
Signed-off-by: Spencer Murray <spencer.murray@sentry.io>
2025-04-28 16:01:48 -04:00
Spencer Murray 41764b9c09 ci: always collect and upload test results, not just on coverage runs
Signed-off-by: Spencer Murray <spencer.murray@sentry.io>
2025-04-28 16:01:47 -04:00
Spencer Murray 191d07d588 ci: add Codecov's test analytics and bundle analysis features
Signed-off-by: Spencer Murray <spencer.murray@sentry.io>
2025-04-28 16:01:43 -04:00
Kate 10a01423ec Merge pull request #52402 from nextcloud/fix/32bit-pack 2025-04-28 21:23:17 +02:00
Joas Schilling 40e52e212a Merge pull request #52434 from nextcloud/bugfix/52420/closure-as-default-app
fix(navigation): Fix default app entry registered as closure
2025-04-28 16:54:11 +02:00
Andy Scherzinger 0d19a1333f Merge pull request #52443 from nextcloud/reminder-dont-validiate-node-dav
fix: don't check node access again for listing reminders in dav
2025-04-28 14:51:25 +02:00
Ferdinand Thiessen a22171507a fix(32bit): use PHP_INT_MAX where needed
* Typo from https://github.com/nextcloud/server/pull/52392

`0xFFFF` is only 2 bytes, but we need either `0xFFFFFFFF` or maybe a bit
easier to read `PHP_INT_MAX`.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-04-28 12:33:58 +02:00
Ferdinand Thiessen e33fcfddc1 fix(dav): throw invalid argument when property type does not match
* Resolves https://github.com/nextcloud/server/issues/49972

Currently a TypeError is thrown when casting fails,
this lead to a HTTP 500 error. Instead throw a proper
InvalidArgumentError so the user receives a HTTP 400.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-04-27 23:05:07 +02:00
Robert Zenz f428e6786b fix(JSResourceLocator) fix incorrect parameter being used. 2025-04-26 15:19:55 +02:00
Lukas Schaefer 99e8849b6c feat(TaskProcessing): Add TextToSpeech provider
Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>
2025-04-26 00:46:23 -04:00
Robin Appelman 3d36625af6 fix: don't check node access again for listing reminders in dav
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-04-25 20:34:58 +02:00
Robin Appelman bb4cf2830a fix: better error message when trying to scan a folder that is already being scanned
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-04-25 18:40:03 +02:00
Joas Schilling cdfce26ea9 fix(navigation): Fix absolute URLs from default apps
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-04-25 14:49:19 +02:00
Joas Schilling 553870d30a fix(navigation): Fix default app entry registered as closure
Signed-off-by: Joas Schilling <coding@schilljs.com>
2025-04-25 14:47:33 +02:00
Maxence Lange 65a2b3b689 fix(metadata): set background job on fresh setup
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
2025-04-23 13:57:25 -01:00
Ferdinand Thiessen 2020760f88 fix(files_versions): wait for version to be created before setting metadata
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-04-23 13:09:43 +02:00
SebastianKrupinski 1a90998edc fix: do not ignore move command object target uri
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
2025-04-16 17:43:12 -04:00
Robin Appelman fb3e60862f fix: disable direct download for shares
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-04-11 17:42:21 +02:00
Robin Appelman 1185b5ee35 fix: don't return other mounts from share mount provider
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-04-11 14:20:21 +02:00
Robin Appelman c1dbc27fd6 fix: don't construct storage when checking if a sub-mount is applicable
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-04-08 18:21:41 +02:00
Robin Appelman 8aae332622 feat: store original storage id and path in object store metadata
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-03-31 15:10:58 +02:00
Robin Appelman 94114b99f7 feat: more generic way of passing metadata to object storage backends for new objects
Signed-off-by: Robin Appelman <robin@icewind.nl>
2025-03-31 14:55:18 +02:00
1127 changed files with 14330 additions and 8000 deletions
+2 -4
View File
@@ -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
View File
@@ -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:
+47 -1
View File
@@ -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
+9 -2
View File
@@ -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: |
+18 -4
View File
@@ -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: |
+9 -2
View File
@@ -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: |
+9 -2
View File
@@ -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]
+9 -2
View File
@@ -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]
+9 -2
View File
@@ -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 ]
+5 -1
View File
@@ -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]
+8 -2
View File
@@ -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: |
+8 -2
View File
@@ -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: |
+8 -2
View File
@@ -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: |
+8 -2
View File
@@ -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
+8 -2
View File
@@ -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: |
+8 -2
View File
@@ -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: |
+8 -2
View File
@@ -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: |
+8 -2
View File
@@ -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: |
+8 -2
View File
@@ -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
+8 -2
View File
@@ -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
+8 -2
View File
@@ -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: |
+2 -21
View File
@@ -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 {
+8
View File
@@ -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;");
+6
View File
@@ -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'],
+19 -30
View File
@@ -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();
+2
View File
@@ -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",
+2
View File
@@ -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,
+3
View File
@@ -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>
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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();
});
+6 -5
View File
@@ -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
View File
@@ -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.",
+8 -8
View File
@@ -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.",
+6 -6
View File
@@ -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",
+6 -6
View File
@@ -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",
+37
View File
@@ -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",
+37
View File
@@ -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;
}
+95 -16
View File
@@ -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);
}
}
+9 -8
View File
@@ -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;
+37 -1
View File
@@ -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;
}
}
}
}
+23
View File
@@ -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';
+2 -1
View File
@@ -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)) {
+1 -1
View File
@@ -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,
];
}
}
+95
View File
@@ -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;
}
}
+131
View File
@@ -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'];
+21 -1
View File
@@ -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) {
+28 -9
View File
@@ -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) {
}
}
}
+6 -5
View File
@@ -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;
}
+6 -3
View File
@@ -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;
+65 -13
View File
@@ -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),
+41 -8
View File
@@ -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;
}));
}
}
+14
View File
@@ -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);
}
}
+1 -1
View File
@@ -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;
}
+7 -1
View File
@@ -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 ?? ''
);
+105 -26
View File
@@ -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')) {
+1
View File
@@ -160,6 +160,7 @@ class RootCollection extends SimpleCollection {
Server::get(CleanupService::class),
$rootFolder,
$userSession,
$shareManager,
);
$uploadCollection->disableListing = $disableListing;
+2
View File
@@ -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,
)
+4 -6
View File
@@ -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]);
}
}
+9 -1
View File
@@ -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,
);
}
/**
+2 -1
View File
@@ -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() {
+27 -8
View File
@@ -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) {
+5 -1
View File
@@ -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
);
}
}
+57 -14
View File
@@ -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');
}
}
+14 -12
View File
@@ -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');
}
}
+3
View File
@@ -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