Compare commits

..

208 Commits

Author SHA1 Message Date
Git'Fellow 21416b8a31 refactor(dbal): move to modern calls
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2026-03-13 09:20:00 +01:00
Benjamin Gaussorgues 3904da98e6 Merge pull request #58864 from nextcloud/fix/cached_exporter_hardening 2026-03-11 18:30:22 +01:00
Côme Chilliet 9b54383532 Merge pull request #58835 from nextcloud/chore/fix-merge-leftover
Remove leftover useless path assignements in View
2026-03-11 16:52:55 +01:00
Joas Schilling 78cfe3742a Merge pull request #58853 from nextcloud/bugfix/noid/remove-wildcard-csp
fix(videoverification): Remove CSP wildcard for video verification
2026-03-11 15:25:59 +01:00
Benjamin Gaussorgues 509784cff2 chore(metrics): harden Cached exporter
Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
2026-03-11 13:01:21 +01:00
Hamza f0f38fd04c Merge pull request #58862 from nextcloud/fix/add-note-example-contact
fix:(dav): add note to example contact
2026-03-11 12:19:41 +01:00
Hamza 9f5c3e39a7 fix:(dav): add note to example contact
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
2026-03-11 11:21:37 +01:00
Joas Schilling a304a54775 Merge pull request #58836 from nextcloud/feat/noid/unify-generated-by-ai-tag-handling
feat(systemtags): Add methods to directly do "Generated by AI" tag
2026-03-11 10:02:20 +01:00
Nextcloud bot 1a70d192f3 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-11 00:26:43 +00:00
Joas Schilling dc529fa86d fix(videoverification): Remove CSP wildcard for video verification
spreed does it via AddContentSecurityPolicyEvent event by now

Signed-off-by: Joas Schilling <coding@schilljs.com>
2026-03-10 22:35:43 +01:00
Côme Chilliet a119716a7d Merge pull request #58826 from nextcloud/carl/code-cleaning-storage-cache
refactor(cache-storage): Make Storage and StorageGlobal psalm strict
2026-03-10 22:01:43 +01:00
Kate 44ec45e941 Merge pull request #58841 from nextcloud/chore/deps/haze-2.2.0 2026-03-10 19:22:20 +01:00
Robin Appelman 38a42d9ad4 Merge pull request #53733 from nextcloud/copy-all-permissions
fix: give target file all permissions on copy
2026-03-10 18:09:33 +01:00
provokateurin aa65c7598e chore(deps): Update haze to 2.2.0
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-03-10 16:25:56 +01:00
Robin Appelman cf124e7c75 fix: make objectstore copy consistent with changed local storage behavior
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-10 16:07:38 +01:00
Raphael Gradenwitz 3e78bf662d Merge pull request #58648 from nextcloud/feat/add-files-sharing-raw-to-rootUrlApps
feat(routing): add files_sharing_raw to rootUrlApps
2026-03-10 15:31:02 +01:00
Carl Schwan 783e2ac625 refactor(external-storage-services): Modernize code
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-03-10 15:27:46 +01:00
Robin Appelman 7dccf5f257 fix: give target file all permissions on copy
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-10 15:27:35 +01:00
Joas Schilling 1425df49d0 feat(systemtags): Add methods to directly do "Generated by AI" tag
Signed-off-by: Joas Schilling <coding@schilljs.com>
2026-03-10 15:19:30 +01:00
Côme Chilliet b0240a43af chore: Remove other useless assignment of 'path' key for FileInfo
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-10 15:12:06 +01:00
Côme Chilliet 347d786a14 chore: Remove useless assignement
Because of the order we merged PRs, this line was not removed while the
 value is not used anymore, let’s cleanup.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-10 15:09:54 +01:00
Côme Chilliet 8417db5df8 Merge pull request #58687 from nextcloud/carl/webhook-typo
fix(webhook_listeners): Fix typo in comment
2026-03-10 15:07:57 +01:00
Kate 810caa3e4a Merge pull request #58593 from nextcloud/carl/share-spring-cleaning 2026-03-10 15:01:50 +01:00
Côme Chilliet c98244a79e Merge pull request #58827 from nextcloud/fix/fix-fileinfo-path
fix(files): Fix FileInfo['path'] situation
2026-03-10 15:01:17 +01:00
Côme Chilliet 17ef1dbec9 Merge pull request #58808 from nextcloud/carl/remove-many-get-server
refactor: remove long deprecated IServerContainer methods
2026-03-10 15:00:48 +01:00
Carl Schwan eb5c6acf13 fix(webhook_listeners): Fix typo in comment
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-03-10 14:39:30 +01:00
Carl Schwan 140bfa2d1f refactor(cache-storage): Make Storage and StorageGlobal psalm strict
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-03-10 14:20:31 +01:00
Côme Chilliet 082664c0c2 chore: Run rector on integration bootstrap code
This avoids integration PHP code to use deprecated method and crash when
 we remove them.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-10 14:15:30 +01:00
Côme Chilliet 397454ff4a fix: Adapt ViewTest to ['path'] being absolute
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-10 12:34:50 +01:00
Côme Chilliet 842b3f9e0a fix(files): Fix FileInfo['path'] situation
Previously $fileInfo->getPath() and $fileInfo['path'] would return
 different things. The [] version was not consistent, being sometimes
 relative and sometimes kind of absolute, and sometimes plenty wrong
 (like when used from occ commands, because there is no user in
 session).
So this is always returning absolute now.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-10 11:56:31 +01:00
Carl Schwan 7fa44717e0 refactor(share): Remove some deprecated method usages
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-03-10 11:39:57 +01:00
Carl Schwan 39c14c383b refactor: remove long deprecated IServerContainer methods
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-03-10 10:36:35 +01:00
Carl Schwan af94028bdb refactor: Remove old Share backend
This has been implicitely deprecated for a while with Share20 containing
the new implementation.

The only use was to determine whether remote sharing was enabled or not,
which we can do much more easily.

Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-03-10 10:09:40 +01:00
Nextcloud bot 6f1fc07868 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-10 00:19:19 +00:00
Kate 27c438503b Merge pull request #58817 from nextcloud/fix/noid/add-fallback-to-raw-path-info 2026-03-09 23:21:13 +01:00
Kate 7ae8aebab3 Merge pull request #58724 from nextcloud/fix/respect-skipping-certificate-verify
fix(files_sharing): respect config to skip certificate verification
2026-03-09 23:08:34 +01:00
Anna Larch cbe8e4d90f fix: add fallback to raw path info
Follow up to https://github.com/nextcloud/server/pull/56843

The raw path info method has no fallback for an empty array parameter

Signed-off-by: Anna Larch <anna@nextcloud.com>
2026-03-09 22:22:08 +01:00
Salvatore Martire 8b235c8f49 fix(files_sharing): respect config to skip certificate verification
This is important especially for local development, as certificate are
self-signed.

Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2026-03-09 18:21:15 +01:00
Daniel 92cf64f798 Merge pull request #58793 from nextcloud/validatePreviewMimeType
fix(preview): Validate preview MIME type
2026-03-09 17:20:00 +01:00
Kate 938b9ba0ac Merge pull request #58800 from nextcloud/ci/rector-apply/ignore-composer-changes 2026-03-09 16:22:37 +01:00
Andy Scherzinger 56eaf1dbcc Merge pull request #58603 from pymnh/feat/extend-group-search-to-teams
feat(UserPlugin): Include teams in group search
2026-03-09 15:17:41 +01:00
Peymaneh 3985555753 feat(UserPlugin): Include teams in group search
Signed-off-by: Peymaneh <peymaneh@posteo.net>
2026-03-09 13:51:27 +01:00
provokateurin fe612bb4dd ci(rector-apply): Ignore composer changes
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-03-09 11:28:15 +01:00
Carl Schwan 2613f3274b Merge pull request #58790 from nextcloud/chore/remove-oc-app-get-current-app
fix: Deprecate OC_App::getCurrentApp and remove its only use
2026-03-09 10:20:08 +01:00
Anna 715d776649 Merge pull request #58229 from nextcloud/fix/noid/improve-preview-scan-performance
perf(preview): bulk process preview regeneration
2026-03-09 09:05:02 +00:00
Nextcloud bot 15591054ac fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-09 00:19:53 +00:00
Git'Fellow 304c9fbc3b fix(preview): Validate preview MIME type
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2026-03-08 18:57:18 +01:00
Côme Chilliet b878678562 chore: Adapt tests to TemplateLayout constructor changes
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-08 17:57:44 +01:00
Côme Chilliet 521bb9432e fix: Deprecate OC_App::getCurrentApp and remove its only use
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-08 17:00:44 +01:00
Côme Chilliet 6fce5aa3ea Merge pull request #58474 from nextcloud/jtr/chore-drop-OC-JSON
chore: drop OC_JSON
2026-03-08 16:51:12 +01:00
Anna Larch e3c6702cd8 perf(preview): bulk process preview regeneration
Signed-off-by: Anna Larch <anna@nextcloud.com>
2026-03-08 11:09:37 +01:00
Nextcloud bot 9cd3177bb4 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-08 00:19:13 +00:00
Nextcloud bot 6a29ea172b fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-07 00:19:22 +00:00
Carl Schwan ed1b0dcd5e Merge pull request #58725 from nextcloud/jtr/docs-config-mount-file-drop
docs(config): drop legacy `mount_file` option
2026-03-06 14:07:44 +01:00
Carl Schwan bd8aff39f2 Merge branch 'master' into jtr/docs-config-mount-file-drop
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2026-03-06 13:39:08 +01:00
Nextcloud bot fc354f46bb fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-06 00:20:14 +00:00
Côme Chilliet 7b305fbe22 Merge pull request #58705 from nextcloud/local-custom-cache-prefix
feat: Add memcache_customprefix
2026-03-05 16:05:14 +01:00
Côme Chilliet 8aedcb4a82 Merge pull request #58652 from nextcloud/automated/noid/rector-changes
Apply rector changes
2026-03-05 16:03:50 +01:00
Carl Schwan 01fa73831f Merge pull request #58728 from nextcloud/jtr/docs-config-part-file-in-storage-non-chunking
docs(config): `part_file_in_storage` only applies to non-chunked
2026-03-05 15:28:12 +01:00
Robin Appelman b40d230218 Merge pull request #58723 from nextcloud/jtr/docs-config-fs-cache-RO
docs(config): improve `filesystem_cache_readonly` docs
2026-03-05 14:29:08 +01:00
Ferdinand Thiessen 5d84d79c4b Merge pull request #58550 from nextcloud/dav-open-size-not-found
handle case where we can't get the filesize after open in dav get
2026-03-05 13:24:52 +01:00
Robin Appelman 26ba3cac9c fix: handle case where we can't get the filesize after open in dav get
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-05 11:35:52 +01:00
Joas Schilling 85b0dcca27 Merge pull request #58664 from nextcloud/bugfix/noid/workflowengine-fix-checks-list
fix(workflowengine): Fix list of checks being a list instead of array…
2026-03-05 10:49:46 +01:00
Kate 687d82843f Merge pull request #58727 from nextcloud/mounts-cache-duplicate-rootid 2026-03-05 09:46:34 +01:00
Nextcloud bot 4da21d78d0 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-05 00:19:23 +00:00
Sebastian Krupinski 142ddaf146 Merge pull request #58697 from nextcloud/feat/add-calendar-name-to-search-result
feat: add calendar name to search entries
2026-03-04 14:11:54 -05:00
Josh f3ceded754 docs(config): clarify part_file_in_storage only applies to non-chunked
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-04 12:47:25 -05:00
Robin Appelman 9884dec646 fix: fix updating cached mounts with multiple entries for root id
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-04 18:43:55 +01:00
Robin Appelman d14c4082e1 test: add test for updating cached mounts with multiple entries for root id
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-03-04 18:43:35 +01:00
Josh 052d62b83c docs(config): make cs happy fixup
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-04 11:41:36 -05:00
Josh ceb1b7e767 docs(config): drop legacy mount_file option
All logic that used this was completely removed as of #30991

Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-04 11:37:18 -05:00
Josh 2faa5e894b docs(config): improve filesystem_cache_readonly docs
- Add more extensive explanation
- Explain interaction/relation to more commonly used option: `filesystem_check_changes`
- Add warning

Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-04 10:22:11 -05:00
SebastianKrupinski 277a35ee23 feat: add calendar name to search entries
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
2026-03-04 08:44:09 -05:00
Raphael Gradenwitz 44e733bae9 Merge branch 'master' into feat/add-files-sharing-raw-to-rootUrlApps 2026-03-04 12:24:34 +01:00
Kate c29c7023d9 Merge pull request #58204 from nextcloud/bug/noid/remove-external-shares-from-share-list 2026-03-04 07:41:25 +01:00
Nextcloud bot f43a4c62d6 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-04 00:19:34 +00:00
Martin 2a2f42e781 feat: Add memcache_customprefix
Signed-off-by: Martin <31348196+Earl0fPudding@users.noreply.github.com>
2026-03-04 00:19:19 +01:00
Daniel 35ef9b078a Merge pull request #57667 from nextcloud/bug/noid/avoid-error-when-updating-share
fix(sharing): do not notify remote if owner and sharee are local users
2026-03-04 00:03:21 +01:00
Daniel Kesselberg c63eb85966 chore: compile assets
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
2026-03-03 23:45:51 +01:00
Daniel Kesselberg 5c19d5eb26 fix(sharing): remove external shares from view on delete
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
2026-03-03 23:43:37 +01:00
Daniel Kesselberg da7250f05d fix(sharing): do not notify remote if owner and sharee are local users
If both the owner and the sharee are local users, no remote notification is required.
If either the owner or the sharee is not a local user, the change must be propagated.

Based on https://github.com/owncloud/core/pull/37534

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
Co-authored-by: Victor Dubiniuk <victor.dubiniuk@gmail.com>
2026-03-03 23:08:52 +01:00
Anna 9afbad3865 Merge pull request #58699 from nextcloud/fix/noid/dav-capabilities-psalm-return-type
fix(dav): add missing search_supports_creation_time and search_supports_upload_time to Capabilities return type
2026-03-03 20:48:29 +01:00
Cristian Scheid 80184b4d25 fix(dav): add missing search_supports_creation_time and search_supports_upload_time to Capabilities return type
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-03 15:01:12 -03:00
Stephan Orbaugh 074df09caf Merge pull request #58680 from nextcloud/fix/drop-files
fix(files): properly handle dropping files
2026-03-03 18:09:54 +01:00
nextcloud-command 7dba462eec chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-03-03 15:51:07 +00:00
Ferdinand Thiessen 0e0af702ac fix(files): properly handle dropping files
- resolves https://github.com/nextcloud/server/issues/58454

use upload library for dropped files instead of custom logic

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-03-03 15:46:36 +00:00
Stephan Orbaugh f6c79c0d33 Merge pull request #58562 from nextcloud/feat/1699/recent-files-mark-recently-created
feat: set creation_time on file creation and render recently created icon
2026-03-03 16:08:07 +01:00
Ferdinand Thiessen 2909821cb2 Merge pull request #58685 from nextcloud/fix/files-external-inital
fix(files_external): set default values for new configurations
2026-03-03 15:53:01 +01:00
nextcloud-command 3542dbf9a1 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-03-03 14:13:13 +00:00
Ferdinand Thiessen cae14d356b fix(files_external): set default values for new configurations
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-03-03 15:05:17 +01:00
nextcloud-command 3836eb6b0d chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-03-03 13:05:15 +00:00
Cristian Scheid 3458d3150b refactor(recent-search): use d:creationdate instead of c:creation_time and crtime instead of attributes.upload_time
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-03 09:43:21 -03:00
Cristian Scheid 03f93d97e9 refactor(recently-created-icon): use mdi/js path instead of raw svg prop setup() instead of data()
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-03 09:42:26 -03:00
Cristian Scheid 3a21ad408b feat(dav): expose search_supports_creation_time and search_supports_upload_time capabilities
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-03 09:42:26 -03:00
Cristian Scheid 7f89490cef feat: set creation_time on file creation and render recently created icon
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-03-03 09:41:22 -03:00
Joas Schilling 34484b591a Merge pull request #58678 from nextcloud/fix/noid/set-lexicon-strictness-to-ignore
fix: Set files app lexicon strictness to IGNORE
2026-03-03 07:44:13 +01:00
Ferdinand Thiessen fa2660a9d7 Merge pull request #58616 from nextcloud/fix/external-shares
fix(files_sharing): allow strict validation of shares
2026-03-03 02:26:45 +01:00
Nextcloud bot 4d58563991 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-03 00:19:33 +00:00
Marcel Müller d039cb92f9 fix: Set files app lexicon strictness to IGNORE
Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
2026-03-02 22:06:49 +01:00
nextcloud-command 128137dc06 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-03-02 17:52:41 +00:00
Ferdinand Thiessen 882d8324e8 fix(files_sharing): allow strict validation of shares
API response sometimes includes `null` for unset, but unset
(`undefined`) is something different than `null`.
So if `null` is passed we mean `undefined` instead.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-03-02 18:31:46 +01:00
Carl Schwan 5fa368c41c Merge pull request #57100 from nextcloud/fix/remove-iservercontainer-from-user-ldap
fix(user_ldap): Remove usages of deprecated IServerContainer
2026-03-02 17:38:52 +01:00
Andy Scherzinger 0e6c8ec73f Merge pull request #58660 from nextcloud/fix/chunked-uploads-on-shared-folders
fix: check source and target when emitting copy event
2026-03-02 16:50:33 +01:00
Côme Chilliet 279d5c226e chore(user_ldap): Adapt code to LDAP constructor change, remove dead code
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-03-02 16:34:04 +01:00
Carl Schwan 52e8de9482 Merge pull request #58643 from nextcloud/jtr/ci-fix-performance-profiler-branch
ci(performance): fix Unexpected input warning for profiler-branch
2026-03-02 16:17:22 +01:00
Sebastian Krupinski 95e3dba34c Merge pull request #58526 from nextcloud/fix/icon-ratio
fix(theming): Fix favicon and touchicon ratios
2026-03-02 09:04:57 -05:00
Ferdinand Thiessen 3b53e9aeca Merge pull request #58615 from nextcloud/jtr/fix-s3-normalizePath-falsy
fix(s3): prevent "0" path from being treated as root
2026-03-02 14:36:04 +01:00
Simon L. 987a52b87f Merge pull request #58657 from nextcloud/enh/noid/auto-label-reports
feat: add workflow that auto-labels bug reports based on entered content
2026-03-02 14:19:11 +01:00
Joas Schilling 1bc0bd7611 fix(workflowengine): Fix list of checks being a list instead of array<int, …>
Signed-off-by: Joas Schilling <coding@schilljs.com>
2026-03-02 14:17:51 +01:00
Josh 2d39c5a179 test(Storage): avoid falsy directory/filename checks (for now)
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-02 13:44:20 +01:00
Josh 6c1d648f59 chore: fixup/ revert cast in S3ObjectTrait
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-02 13:44:20 +01:00
Josh d5d34adcbb fix(S3ObjectTrait): cast $urn to a string until typing adding elsewhere
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-02 13:44:20 +01:00
Josh ace9eb21e6 test(Storage): expand file / directory name tests
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-02 13:44:20 +01:00
Josh 9ce1ae8e61 fix(ObjectStorage): fix 0 folder handling in Primary Storage
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-02 13:44:20 +01:00
Josh 958e67687b fix(s3): Fix 0 folder handling in External Storage
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-02 13:44:19 +01:00
Côme Chilliet e7c4dbf2cb Merge pull request #58642 from nextcloud/jtr/perf-s3-external-cache-sizing
perf(files_external): Increase S3 cache caps
2026-03-02 13:36:01 +01:00
Côme Chilliet 7f18934344 Merge pull request #58654 from nextcloud/jtr/refactor-external-s3-unused-methods
refactor(files_external/S3): drop unused methods
2026-03-02 13:27:50 +01:00
Salvatore Martire 8431abff32 fix: check source and target when emitting copy event
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2026-03-02 13:15:11 +01:00
Simon L. e0bf0737a0 fix: adjust the bug-report template to be consistent
Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-03-02 12:16:45 +01:00
Simon L. 10ae88a1c3 feat: add workflow that aut-labels bug reports based on entered content
Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-03-02 12:13:37 +01:00
Andy Scherzinger b729dc4ead Merge pull request #58057 from nextcloud/carl/perf-delete-share
perf(sharing): Avoid loading all shares from all users when unsharing
2026-03-02 12:03:52 +01:00
Nextcloud bot 2b5a26db95 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-03-02 00:19:14 +00:00
Carl Schwan 4588c7134d fix: Fix ci after run of rector
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-03-01 22:48:25 +01:00
Josh bde043d13b chore(files_external/s3): drop no-op $this->filesCache use sites
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-01 12:53:57 -05:00
Josh 08b465d219 refactor(files_external/S3): drop unused methods
getContentLength() and getLastModified() are dead code. Originally added in PR #11518 and made defunct in PR #29220.

Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-03-01 12:45:01 -05:00
nextcloud-command 663018455e refactor: Apply rector changes
Signed-off-by: GitHub <noreply@github.com>
2026-03-01 14:43:11 +00:00
github-actions[bot] ee43b68606 Merge pull request #58649 from nextcloud/dependabot/npm_and_yarn/fast-xml-parser-5.4.1
chore(deps): Bump fast-xml-parser from 5.3.6 to 5.4.1
2026-03-01 13:51:14 +01:00
dependabot[bot] ced6d9e430 chore(deps): Bump fast-xml-parser from 5.3.6 to 5.4.1
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 5.3.6 to 5.4.1.
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.3.6...v5.4.1)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-version: 5.4.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 12:05:33 +01:00
github-actions[bot] 62109cc0a1 Merge pull request #58647 from nextcloud/dependabot/github_actions/github-actions-dcec74b321
chore(deps): Bump the github-actions group with 5 updates
2026-03-01 09:54:56 +00:00
dependabot[bot] 0b2893a154 chore(deps): Bump the github-actions group with 5 updates
Bumps the github-actions group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.32.2` | `4.32.4` |
| [cypress-io/github-action](https://github.com/cypress-io/github-action) | `7.1.2` | `7.1.5` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `6.0.0` | `7.0.0` |
| [LizardByte/actions](https://github.com/lizardbyte/actions) | `2026.212.22356` | `2026.227.200013` |
| [actions/stale](https://github.com/actions/stale) | `10.1.1` | `10.2.0` |


Updates `github/codeql-action` from 4.32.2 to 4.32.4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2...89a39a4e59826350b863aa6b6252a07ad50cf83e)

Updates `cypress-io/github-action` from 7.1.2 to 7.1.5
- [Release notes](https://github.com/cypress-io/github-action/releases)
- [Changelog](https://github.com/cypress-io/github-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/github-action/compare/84d178e4bbce871e23f2ffa3085898cde0e4f0ec...bc22e01685c56e89e7813fd8e26f33dc47f87e15)

Updates `actions/upload-artifact` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

Updates `LizardByte/actions` from 2026.212.22356 to 2026.227.200013
- [Release notes](https://github.com/lizardbyte/actions/releases)
- [Commits](https://github.com/lizardbyte/actions/compare/9bf3ef783775e17fe6b8dde3585d94ec570b93c2...70bb8d394d1c92f6113aeec6ae9cc959a5763d15)

Updates `actions/stale` from 10.1.1 to 10.2.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/997185467fa4f803885201cee163a9f38240193d...b5d41d4e1d5dceea10e7104786b73624c18a190f)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: cypress-io/github-action
  dependency-version: 7.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: LizardByte/actions
  dependency-version: 2026.227.200013
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/stale
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 23:14:04 +00:00
github-actions[bot] e13e0c598e Merge pull request #58641 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/fast-xml-parser-5.4.1
chore(deps): Bump fast-xml-parser from 5.3.6 to 5.4.1 in /build/frontend-legacy
2026-02-28 22:47:13 +01:00
github-actions[bot] 60ea64210e Merge pull request #58599 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/multi-571506d351
chore(deps): Bump rollup in /build/frontend-legacy
2026-02-28 22:45:47 +01:00
nextcloud-command b42b92c13e chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-02-28 21:09:50 +00:00
dependabot[bot] 448e6df7e8 chore(deps): Bump fast-xml-parser in /build/frontend-legacy
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 5.3.6 to 5.4.1.
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.3.6...v5.4.1)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-version: 5.4.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 21:07:35 +00:00
dependabot[bot] a0bd65b174 chore(deps): Bump rollup in /build/frontend-legacy
Bumps  and [rollup](https://github.com/rollup/rollup). These dependencies needed to be updated together.

Updates `rollup` from 4.52.3 to 4.59.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.3...v4.59.0)

Updates `rollup` from 2.79.2 to 2.80.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.3...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
- dependency-name: rollup
  dependency-version: 2.80.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 22:04:15 +01:00
github-actions[bot] c113a7698b Merge pull request #58640 from nextcloud/dependabot/npm_and_yarn/rollup-4.59.0
chore(deps): Bump rollup from 4.52.5 to 4.59.0
2026-02-28 20:54:01 +00:00
nextcloud-command 16592b9f73 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-02-28 19:57:23 +00:00
dependabot[bot] a49ceee5ae chore(deps): Bump rollup from 4.52.5 to 4.59.0
Bumps [rollup](https://github.com/rollup/rollup) from 4.52.5 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.5...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 20:49:28 +01:00
Andy Scherzinger ea24a7f3b8 Merge pull request #58621 from nextcloud/dependabot/npm_and_yarn/stylelint-17.4.0
chore(deps-dev): Bump stylelint from 17.3.0 to 17.4.0
2026-02-28 20:41:06 +01:00
github-actions[bot] 158f643c2b Merge pull request #58624 from nextcloud/dependabot/npm_and_yarn/vue-3.5.29
chore(deps): Bump vue from 3.5.28 to 3.5.29
2026-02-28 19:32:14 +00:00
ernolf b24663db93 feat(routing): add files_sharing_raw to rootUrlApps
- Registers the files_sharing_raw app as a root-URL app so that its routes are served under /raw/{token} and /rss instead of the default /apps/files_sharing_raw/... prefix.
  This is required for the files_sharing_raw app to generate correct canonical raw URLs via PublicUrlBuilder.

Signed-off-by: ernolf <raphael.gradenwitz@googlemail.com>
2026-02-28 20:30:19 +01:00
nextcloud-command f8aea303c2 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-02-28 19:14:22 +00:00
dependabot[bot] e14c8ec2c4 chore(deps): Bump vue from 3.5.28 to 3.5.29
Bumps [vue](https://github.com/vuejs/core) from 3.5.28 to 3.5.29.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.28...v3.5.29)

---
updated-dependencies:
- dependency-name: vue
  dependency-version: 3.5.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 20:06:32 +01:00
github-actions[bot] 4ca998e8bc Merge pull request #58627 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/webpack-5.105.3
chore(deps-dev): Bump webpack from 5.105.2 to 5.105.3 in /build/frontend-legacy
2026-02-28 18:56:59 +00:00
nextcloud-command 05345789dd chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-02-28 18:36:38 +00:00
dependabot[bot] 5ee5a71930 chore(deps-dev): Bump webpack in /build/frontend-legacy
Bumps [webpack](https://github.com/webpack/webpack) from 5.105.2 to 5.105.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.105.2...v5.105.3)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.105.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 19:28:42 +01:00
github-actions[bot] c9af0f0977 Merge pull request #58598 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/multi-f81e89d21b
chore(deps): Bump minimatch in /build/frontend-legacy
2026-02-28 17:35:00 +00:00
github-actions[bot] e00da518e7 Merge pull request #58622 from nextcloud/dependabot/npm_and_yarn/cypress-15.11.0
chore(deps-dev): Bump cypress from 15.10.0 to 15.11.0
2026-02-28 18:25:41 +01:00
nextcloud-command 1aadd46c18 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-02-28 17:11:34 +00:00
dependabot[bot] 9b4f3f2c11 chore(deps): Bump minimatch in /build/frontend-legacy
Bumps  and [minimatch](https://github.com/isaacs/minimatch). These dependencies needed to be updated together.

Updates `minimatch` from 9.0.5 to 9.0.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

Updates `minimatch` from 8.0.4 to 8.0.7
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

Updates `minimatch` from 5.1.6 to 5.1.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

Updates `minimatch` from 10.1.1 to 10.2.4
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 9.0.9
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 8.0.7
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 5.1.9
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 10.2.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 18:03:51 +01:00
github-actions[bot] 36232afffd Merge pull request #58625 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/babel-loader-exclude-node-modules-except-1.2.4
chore(deps-dev): Bump babel-loader-exclude-node-modules-except from 1.2.1 to 1.2.4 in /build/frontend-legacy
2026-02-28 16:58:49 +00:00
github-actions[bot] c864d55f8f Merge pull request #58626 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/libphonenumber-js-1.12.38
chore(deps): Bump libphonenumber-js from 1.12.37 to 1.12.38 in /build/frontend-legacy
2026-02-28 16:46:06 +00:00
Josh a1814676b8 ci(performance): fix Unexpected input warning for profiler-branch
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-02-28 09:38:05 -05:00
Josh 430975ce96 perf(files_external): Increase S3 metadata cache caps
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-02-28 09:08:00 -05:00
nextcloud-command 41f87251c7 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-02-28 12:45:00 +00:00
dependabot[bot] 1098812f95 chore(deps): Bump libphonenumber-js in /build/frontend-legacy
Bumps [libphonenumber-js](https://gitlab.com/catamphetamine/libphonenumber-js) from 1.12.37 to 1.12.38.
- [Changelog](https://gitlab.com/catamphetamine/libphonenumber-js/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/catamphetamine/libphonenumber-js/compare/v1.12.37...v1.12.38)

---
updated-dependencies:
- dependency-name: libphonenumber-js
  dependency-version: 1.12.38
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 13:37:36 +01:00
github-actions[bot] dbdba090ee Merge pull request #58620 from nextcloud/dependabot/composer/vendor-bin/rector/rector/rector-2.3.8
chore(deps-dev): Bump rector/rector from 2.3.7 to 2.3.8 in /vendor-bin/rector
2026-02-28 13:37:10 +01:00
github-actions[bot] 52f133dbc2 Merge pull request #58639 from nextcloud/dependabot/npm_and_yarn/multi-71d1011188
chore(deps): Bump minimatch and editorconfig
2026-02-28 08:58:31 +01:00
dependabot[bot] 54f95a1d8b chore(deps): Bump minimatch and editorconfig
Bumps [minimatch](https://github.com/isaacs/minimatch) and [editorconfig](https://github.com/editorconfig/editorconfig-core-js). These dependencies needed to be updated together.

Updates `minimatch` from 9.0.5 to 9.0.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

Updates `minimatch` from 5.1.6 to 5.1.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

Updates `editorconfig` from 1.0.4 to 1.0.7
- [Release notes](https://github.com/editorconfig/editorconfig-core-js/releases)
- [Changelog](https://github.com/editorconfig/editorconfig-core-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/editorconfig/editorconfig-core-js/compare/v1.0.4...v1.0.7)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 9.0.9
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 5.1.9
  dependency-type: indirect
- dependency-name: editorconfig
  dependency-version: 1.0.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 06:38:18 +00:00
dependabot[bot] 0b01566282 chore(deps-dev): Bump babel-loader-exclude-node-modules-except
Bumps [babel-loader-exclude-node-modules-except](https://github.com/dutchenkoOleg/babel-loader-exclude-node-modules-except) from 1.2.1 to 1.2.4.
- [Release notes](https://github.com/dutchenkoOleg/babel-loader-exclude-node-modules-except/releases)
- [Commits](https://github.com/dutchenkoOleg/babel-loader-exclude-node-modules-except/compare/v1.2.1...v1.2.4)

---
updated-dependencies:
- dependency-name: babel-loader-exclude-node-modules-except
  dependency-version: 1.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 02:07:37 +00:00
dependabot[bot] 60bc7ffd0c chore(deps-dev): Bump cypress from 15.10.0 to 15.11.0
Bumps [cypress](https://github.com/cypress-io/cypress) from 15.10.0 to 15.11.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v15.10.0...v15.11.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-version: 15.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 02:06:33 +00:00
dependabot[bot] c94c6ea45a chore(deps-dev): Bump stylelint from 17.3.0 to 17.4.0
Bumps [stylelint](https://github.com/stylelint/stylelint) from 17.3.0 to 17.4.0.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/17.3.0...17.4.0)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 02:06:20 +00:00
dependabot[bot] b2dd3be343 chore(deps-dev): Bump rector/rector in /vendor-bin/rector
Bumps [rector/rector](https://github.com/rectorphp/rector) from 2.3.7 to 2.3.8.
- [Release notes](https://github.com/rectorphp/rector/releases)
- [Commits](https://github.com/rectorphp/rector/compare/2.3.7...2.3.8)

---
updated-dependencies:
- dependency-name: rector/rector
  dependency-version: 2.3.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 02:04:31 +00:00
Nextcloud bot d758f64a18 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-02-28 00:18:59 +00:00
Marcel Müller 00af1217ff Merge pull request #58601 from nextcloud/fix/noid/default-loglevel-with-match-condition
fix: Use configured loglevel even when log.condition matches is set
2026-02-27 16:00:49 +01:00
Ferdinand Thiessen 694e2312fd Merge pull request #58589 from nextcloud/fix/external-missing
fix(files_external): boolean comparison of array
2026-02-27 15:40:14 +01:00
Ferdinand Thiessen 69a5db46bd fix: code style
Co-authored-by: Carl Schwan <carl@carlschwan.eu>
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-27 14:31:42 +01:00
Kent Delante d6eade0119 Merge pull request #58582 from nextcloud/leftybournes/fix/files_external_delete_objects
fix: pass only object key to deleteObjects call
2026-02-27 11:28:19 +08:00
Kent Delante 8d1cb50048 fix: pass only object key to deleteObjects call
Some S3-compatible object storage hosts don't like the ETag being included in
the request and return a MalformedXML response. In the AWS API documentation,
only the object key is required so just pass that in.

Signed-off-by: Kent Delante <kent.delante@proton.me>
2026-02-27 10:54:23 +08:00
Nextcloud bot b4b328cf61 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-02-27 00:19:48 +00:00
Carl Schwan e47195a334 Merge pull request #58597 from nextcloud/bugfix/58594/files_sharing-disabled
fix(repair): Fix repair steps when files_sharing is disabled
2026-02-27 00:35:03 +01:00
Marcel Müller a56607e91f fix: Use configured loglevel even when log.condition matches is set
Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
2026-02-26 23:12:20 +01:00
github-actions[bot] 4d00f49757 Merge pull request #58556 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/multi-5543462fab
chore(deps): Bump bn.js in /build/frontend-legacy
2026-02-26 21:58:10 +01:00
Andy Scherzinger 83d795dd18 Merge pull request #58595 from nextcloud/fix/db-occ-pending-migrations-typo
fix(db): pending migrations in `occ migrations:status`
2026-02-26 21:56:27 +01:00
Joas Schilling a6b9483a5f fix(repair): Fix repair steps when files_sharing is disabled
Signed-off-by: Joas Schilling <coding@schilljs.com>
2026-02-26 21:51:13 +01:00
Josh 636345bac8 fix(db): pending migrations in occ migrations:status
Fixes #58569

Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-02-26 12:38:00 -05:00
Benjamin Gaussorgues 79d4953e64 Merge pull request #58535 from nextcloud/fix/do-not-send-headers-on-cli 2026-02-26 16:20:52 +01:00
Côme Chilliet d8084559a9 chore: Add missing return type in tests
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-02-26 15:22:09 +01:00
Côme Chilliet 60c86848e4 fix: Use proper DI for LDAP class
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-02-26 15:21:40 +01:00
Sebastian Krupinski ca8050b94e Merge pull request #58008 from nextcloud/feat/calendar-federation-readwrite
feat: calendar read and write federation
2026-02-26 09:01:25 -05:00
Carl Schwan 47b08a07d8 Merge pull request #58586 from nextcloud/carl/sharehelper-typing
refactor(typing): Correct typing of IShareHelper
2026-02-26 14:21:53 +01:00
Ferdinand Thiessen d50fbfe37d fix(files_external): boolean comparison of array
To check if there are no missing required dependencies we need to check
if the required dependencies are **empty** because `!array` is still
true.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-26 14:21:50 +01:00
Kate 77c070bc93 Merge pull request #58559 from nextcloud/fix/itypedquerybuilder/chained-calls 2026-02-26 13:44:40 +01:00
Côme Chilliet cc365554c6 chore: update psalm baseline
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-02-26 13:31:27 +01:00
Côme Chilliet 8b432809b4 fix: Adapt LDAPProvider tests to code changes
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-02-26 13:31:26 +01:00
Côme Chilliet c6f56ddbd7 fix(user_ldap): Remove usages of deprecated IServerContainer
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-02-26 13:31:26 +01:00
Kent Delante 09c9241b30 Merge pull request #58042 from nextcloud/feat/clear_password
feat(occ): allow admins to clear account passwords
2026-02-26 20:29:26 +08:00
Kent Delante 711bd2bc6d feat(occ): allow admins to clear account passwords
Signed-off-by: Kent Delante <kent.delante@proton.me>
2026-02-26 19:36:22 +08:00
Carl Schwan 2a81cba978 refactor(typing): Correct typing of IShareHelper
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-02-26 12:22:44 +01:00
Andy Scherzinger 6df490942c Merge pull request #58525 from nextcloud/fix/fix-decryption-failure-false-positive
fix(encryption): Improve type strictness on decryption check
2026-02-26 12:10:01 +01:00
Andy Scherzinger 0b8e7bb4f0 Merge pull request #58205 from nextcloud/bug-show-configuration-options-for-again
fix: show configuration options for external storage backends
2026-02-26 11:56:41 +01:00
Carl Schwan 4acb3b5da3 perf: Perform share path validation early
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-02-26 11:38:59 +01:00
Carl Schwan 0676fba514 perf(sharing): Avoid loading all shares from all users when unsharing
First check which users have a shares and for which providers and then
only load these shares.

Avoid doing at most 5 SQL queries for each users a share was shared with if
there are no shares.

Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-02-25 17:14:15 +01:00
Salvatore Martire 7e264ba58e Merge pull request #58571 from nextcloud/fix/regional-languages
fix(L10N): stop stripping _ from language codes
2026-02-25 16:54:18 +01:00
SebastianKrupinski 64f319ab4e feat: calendar federation write
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
2026-02-25 10:35:27 -05:00
Salvatore Martire ec0ed788fa fix(L10N): stop stripping _ from language codes
Stripping the underscore breaks support for all languages like de_AT,
de_DE and so on...

Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2026-02-25 15:28:34 +01:00
Daniel Kesselberg 67d1fac6f6 fix: show configuration options for external storage backends
The occ files_external_backends command is supposed to list available backends along with their configuration options.

For the SMB backend, only the timeout option is currently shown, while options like host, share, root, and domain are missing. This makes configuring external storage via occ complicated.

Since the timeout option is marked as hidden but still shown, while the other options are not, the logic needs to be inverted so that all relevant configuration options are displayed correctly.

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
2026-02-25 15:24:20 +01:00
Kate af98eed523 Merge pull request #58548 from nextcloud/artonge/feat/ai_pr_template 2026-02-25 11:52:12 +01:00
Louis Chmn bfac9e7023 feat: Add AI checkbox to pull request template
Request committers to be transparent with their usage of AI.

Signed-off-by: Louis <louis@chmn.me>
Signed-off-by: Louis Chmn <louis@chmn.me>
2026-02-25 10:41:38 +01:00
provokateurin 237d5156b6 fix(ITypedQueryBuilder): Add correct return type and add note about Psalm bug
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-25 09:52:48 +01:00
provokateurin 40c39270c0 fix((ITypedQueryBuilder): Fix chained calls of non-select methods
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-25 09:51:59 +01:00
dependabot[bot] f5b18dd7fd chore(deps): Bump bn.js in /build/frontend-legacy
Bumps  and [bn.js](https://github.com/indutny/bn.js). These dependencies needed to be updated together.

Updates `bn.js` from 5.2.2 to 5.2.3
- [Release notes](https://github.com/indutny/bn.js/releases)
- [Changelog](https://github.com/indutny/bn.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/indutny/bn.js/compare/v5.2.2...v5.2.3)

Updates `bn.js` from 4.12.2 to 4.12.3
- [Release notes](https://github.com/indutny/bn.js/releases)
- [Changelog](https://github.com/indutny/bn.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/indutny/bn.js/compare/v5.2.2...v5.2.3)

---
updated-dependencies:
- dependency-name: bn.js
  dependency-version: 5.2.3
  dependency-type: indirect
- dependency-name: bn.js
  dependency-version: 4.12.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-24 21:02:56 +00:00
Côme Chilliet f885d7292f fix(occ): Do not attempt to send headers on CLI
This avoids errors like 'Cannot modify header information - headers already sent',
 when using --debug-log with occ.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-02-24 10:22:13 +01:00
SebastianKrupinski 3bd7784c1e fix(theming): Fix favicon and touchicon ratios
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
2026-02-23 10:38:50 -05:00
Côme Chilliet e4244c5fc8 fix(encryption): Improve type strictness on decryption check
Otherwise decrypting a falsy value like '0' would be seen as a
 decryption failure.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-02-23 14:49:03 +01:00
Josh 2475a0bb19 chore(autoload_classmap): drop OC_JSON
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-02-20 16:58:21 -05:00
Josh 7f9b9e9104 chore(autoload_static): drop OC_JSON
Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-02-20 16:57:45 -05:00
Josh aad7571502 chore: drop OC_JSON
Closes #8568

Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-02-20 16:56:24 -05:00
1072 changed files with 8097 additions and 6709 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ body:
options:
- "32"
- "33"
- "master"
- "34 (master)"
validations:
required: true
- type: dropdown
+4
View File
@@ -23,3 +23,7 @@
- [ ] [Backports requested](https://github.com/nextcloud/backportbot/#usage) where applicable (ex: critical bugfixes)
- [ ] [Labels added](https://github.com/nextcloud/server/labels) where applicable (ex: bug/enhancement, `3. to review`, feature component)
- [ ] [Milestone added](https://github.com/nextcloud/server/milestones) for target branch/version (ex: 32.x for `stable32`)
## AI (if applicable)
- [ ] The content of this PR was partly or fully generated using AI
+45
View File
@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
name: Auto-label bug reports
on:
issues:
types: [opened]
jobs:
add-version-label:
if: contains(github.event.issue.title, '[Bug]')
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Extract version number and apply label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const body = context.payload.issue.body || '';
const normalizedBody = body.replace(/\r\n?/g, '\n');
let label = '';
// Extract Nextcloud Server version number from a block like:
// ### Nextcloud Server version
// 32
const versionMatch = normalizedBody.match(/### Nextcloud Server version\s*\n+([0-9]{1,3})\b/);
let nextcloudVersion = null;
if (versionMatch) {
nextcloudVersion = parseInt(versionMatch[1], 10);
label = nextcloudVersion + '-feedback';
}
if (label) {
try {
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [label]
});
} catch (error) {
core.setFailed(`Failed to add label "${label}": ${error.message || error}`);
}
}
+2 -2
View File
@@ -37,13 +37,13 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
category: "/language:${{matrix.language}}"
+3 -3
View File
@@ -171,7 +171,7 @@ jobs:
run: ./node_modules/cypress/bin/cypress install
- name: Run ${{ matrix.containers == 'component' && 'component' || 'E2E' }} cypress tests
uses: cypress-io/github-action@84d178e4bbce871e23f2ffa3085898cde0e4f0ec # v7.1.2
uses: cypress-io/github-action@bc22e01685c56e89e7813fd8e26f33dc47f87e15 # v7.1.5
with:
# We already installed the dependencies in the init job
install: false
@@ -195,7 +195,7 @@ jobs:
SETUP_TESTING: ${{ matrix.containers == 'setup' && 'true' || '' }}
- name: Upload snapshots and videos
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: snapshots_${{ matrix.containers }}
@@ -218,7 +218,7 @@ jobs:
run: docker exec nextcloud-e2e-test-server_${{ env.APP_NAME }} tar -cvjf - data > data.tar
- name: Upload data archive
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: failure() && matrix.containers != 'component'
with:
name: nc_data_${{ matrix.containers }}
+1 -1
View File
@@ -71,7 +71,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: LizardByte/actions/actions/setup_python@9bf3ef783775e17fe6b8dde3585d94ec570b93c2 # v2026.212.22356
uses: LizardByte/actions/actions/setup_python@70bb8d394d1c92f6113aeec6ae9cc959a5763d15 # v2026.227.200013
with:
python-version: '2.7'
+3 -3
View File
@@ -63,7 +63,7 @@ jobs:
ref: ${{ github.event.pull_request.head.ref }}
- name: Run before measurements
uses: nextcloud/profiler@6801ee10fc80f10b444388fb6ca9b36ad8a2ea83
uses: nextcloud/profiler@6a74c915048285b35b8e1cd96c0835a635945044
with:
run: |
curl -s -X PROPFIND -u test:test http://localhost:8080/remote.php/dav/files/test
@@ -85,7 +85,7 @@ jobs:
- name: Run after measurements
id: compare
uses: nextcloud/profiler@6801ee10fc80f10b444388fb6ca9b36ad8a2ea83
uses: nextcloud/profiler@6a74c915048285b35b8e1cd96c0835a635945044
with:
run: |
curl -s -X PROPFIND -u test:test http://localhost:8080/remote.php/dav/files/test
@@ -99,7 +99,7 @@ jobs:
- name: Upload profiles
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
with:
name: profiles
path: |
+1
View File
@@ -50,6 +50,7 @@ jobs:
run: |
composer remove nextcloud/ocp --dev --no-scripts
composer i
git restore lib/composer/composer
- name: Rector
run: composer run rector
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
issues: write
steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v9
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v9
with:
repo-token: ${{ secrets.COMMAND_BOT_PAT }}
stale-issue-message: >
+1 -1
View File
@@ -88,7 +88,7 @@ jobs:
- name: Upload Security Analysis results to GitHub
if: always()
uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v3
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3
with:
sarif_file: results.sarif
+1 -1
View File
@@ -23,7 +23,7 @@ OC.L10N.register(
"Could not reload comments" : "Kon reactie niet opnieuw laden",
"Failed to mark comments as read" : "Kon reacties niet als gelezen markeren",
"Unable to load the comments list" : "Kan reactielijst niet laden",
"No comments yet, start the conversation!" : "Nog geen reacties, start de discussie!",
"No comments yet, start the conversation!" : "Nog geen reacties, start het gesprek!",
"No more messages" : "Geen berichten meer",
"Retry" : "Opnieuw proberen",
"_1 new comment_::_{unread} new comments_" : ["1 nieuwe reactie","{unread} nieuwe reacties"],
+1 -1
View File
@@ -21,7 +21,7 @@
"Could not reload comments" : "Kon reactie niet opnieuw laden",
"Failed to mark comments as read" : "Kon reacties niet als gelezen markeren",
"Unable to load the comments list" : "Kan reactielijst niet laden",
"No comments yet, start the conversation!" : "Nog geen reacties, start de discussie!",
"No comments yet, start the conversation!" : "Nog geen reacties, start het gesprek!",
"No more messages" : "Geen berichten meer",
"Retry" : "Opnieuw proberen",
"_1 new comment_::_{unread} new comments_" : ["1 nieuwe reactie","{unread} nieuwe reacties"],
+1 -1
View File
@@ -89,7 +89,7 @@ $server->httpRequest->setUrl(Server::get(IRequest::class)->getRequestUri());
/** @var string $baseuri defined in remote.php */
$server->setBaseUri($baseuri);
// Add plugins
$server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), \OCP\Server::get(IL10nFactory::class)->get('dav')));
$server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), Server::get(IL10nFactory::class)->get('dav')));
$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend));
$server->addPlugin(new Plugin());
@@ -77,6 +77,7 @@ return array(
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarFactory' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarFactory.php',
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php',
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php',
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarObject' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarObject.php',
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => $baseDir . '/../lib/CalDAV/Federation/FederationSharingService.php',
'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarFederationProtocolV1' => $baseDir . '/../lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php',
@@ -92,6 +92,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarFactory' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarFactory.php',
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php',
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php',
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarObject.php',
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederationSharingService.php',
'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarFederationProtocolV1' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php',
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Dateigröße konnte nicht überprüft werden: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Datei konnte nicht geöffnet werden: %1$s (%2$d), Datei scheint aber zu existieren",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Datei konnte nicht geöffnet werden: %1$s (%2$d), Datei scheint nicht zu existieren",
"Failed to get size for : %1$s" : "Größe konnte nicht ermittelt werden für: %1$s",
"Encryption not ready: %1$s" : "Verschlüsselung nicht bereit: %1$s",
"Failed to open file: %1$s" : "Datei konnte nicht geöffnet werden: %1$s",
"Failed to unlink: %1$s" : "Fehler beim Aufheben der Verknüpfung: %1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "Dateigröße konnte nicht überprüft werden: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Datei konnte nicht geöffnet werden: %1$s (%2$d), Datei scheint aber zu existieren",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Datei konnte nicht geöffnet werden: %1$s (%2$d), Datei scheint nicht zu existieren",
"Failed to get size for : %1$s" : "Größe konnte nicht ermittelt werden für: %1$s",
"Encryption not ready: %1$s" : "Verschlüsselung nicht bereit: %1$s",
"Failed to open file: %1$s" : "Datei konnte nicht geöffnet werden: %1$s",
"Failed to unlink: %1$s" : "Fehler beim Aufheben der Verknüpfung: %1$s",
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Dateigröße konnte nicht überprüft werden: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Datei konnte nicht geöffnet werden: %1$s (%2$d), Datei scheint aber zu existieren",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Datei konnte nicht geöffnet werden: %1$s (%2$d), Datei scheint nicht zu existieren",
"Failed to get size for : %1$s" : "Größe konnte nicht ermittelt werden für: %1$s",
"Encryption not ready: %1$s" : "Verschlüsselung nicht bereit: %1$s",
"Failed to open file: %1$s" : "Datei konnte nicht geöffnet werden: %1$s",
"Failed to unlink: %1$s" : "Fehler beim Aufheben der Verknüpfung: %1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "Dateigröße konnte nicht überprüft werden: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Datei konnte nicht geöffnet werden: %1$s (%2$d), Datei scheint aber zu existieren",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Datei konnte nicht geöffnet werden: %1$s (%2$d), Datei scheint nicht zu existieren",
"Failed to get size for : %1$s" : "Größe konnte nicht ermittelt werden für: %1$s",
"Encryption not ready: %1$s" : "Verschlüsselung nicht bereit: %1$s",
"Failed to open file: %1$s" : "Datei konnte nicht geöffnet werden: %1$s",
"Failed to unlink: %1$s" : "Fehler beim Aufheben der Verknüpfung: %1$s",
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Failed to check file size: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Could not open file: %1$s (%2$d), file does seem to exist",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Could not open file: %1$s (%2$d), file doesn't seem to exist",
"Failed to get size for : %1$s" : "Failed to get size for : %1$s",
"Encryption not ready: %1$s" : "Encryption not ready: %1$s",
"Failed to open file: %1$s" : "Failed to open file: %1$s",
"Failed to unlink: %1$s" : "Failed to unlink: %1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "Failed to check file size: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Could not open file: %1$s (%2$d), file does seem to exist",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Could not open file: %1$s (%2$d), file doesn't seem to exist",
"Failed to get size for : %1$s" : "Failed to get size for : %1$s",
"Encryption not ready: %1$s" : "Encryption not ready: %1$s",
"Failed to open file: %1$s" : "Failed to open file: %1$s",
"Failed to unlink: %1$s" : "Failed to unlink: %1$s",
+10 -10
View File
@@ -254,16 +254,16 @@ OC.L10N.register(
"Due on %s" : "Tähtaeg: %s",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Tere tulemast Nextcloudi Kalendrisse!\n\nSee näidissündmus võimaldab sul tutvuda Nextcloudi Kalendri paindlikkusega oma aja plaanimisel - proovi teha igasuguseid muudatusi!\n\nNextcloudi Kalendriga saad sa:\n- vaevata luua, muuta ja hallata sündmusi,\n- koostada mitmeid kalendreid ning neid jagada tiimikaaslaste, sõprade ja perega,\n- kontrollida teiste vabu aega ja enda omi näidata teistele,\n- kasutada sujuvat CalDAV-i põhist lõimingut teiste rakenduste ja seadmetega,\n- kohendada kõike oma vajadustele: ajastades korduvaid sündmusi ning sättida teavitusi ja muid seadistusi.",
"Example event - open me!" : "Näidissündmus - klõpsi mind!",
"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.",
"Enable System Address Book" : "Kasuta süsteemset aadressiraamatut",
"DAV system address book" : "DAV-i süsteemne aadressiraamat",
"No outstanding DAV system address book sync." : "Pole DAV-i süsteemse aadressiraamatu sünkroniseerimist.",
"The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Kuna selles serveris on üle 1000 kasutaja, siis DAV-i süsteemse aadressiraamatu sünkroonomist poel veel toimunud. Aga võis ka juhtuda viga. Palun käivita ta käsurealt ise käsuga „occ dav:sync-system-addressbook“.",
"DAV system address book size" : "DAV-i süsteemse aadressiraamatu suurus",
"The system address book is disabled" : "Süsteemne aadressiraamat pole kasutusel",
"The system address book is enabled, but contains more than the configured limit of %d contacts" : "Süsteemne aadressiraamat on kasutusel, kuid seal on andmeid rohkem, kui seadistatud %d kontakti ülempiir lubab",
"The system address book is enabled and contains less than the configured limit of %d contacts" : "Süsteemne aadressiraamat on kasutusel ning seal on andmeid vähem, kui seadistatud %d kontakti ülempiir lubab",
"System Address Book" : "Süsteemiülene aadressiraamat",
"The system address book contains contact information for all users in your instance." : "Süsteemiüleses aadressiraamatus leiduvad kõikide selle serveri kasutajate kontaktandmed.",
"Enable System Address Book" : "Kasuta süsteemiülest aadressiraamatut",
"DAV system address book" : "DAV-i süsteemiülene aadressiraamat",
"No outstanding DAV system address book sync." : "DAV-i süsteemiülese aadressiraamatu sünkroonimist pole ootel või toimunud.",
"The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Kuna selles serveris on üle 1000 kasutaja, siis DAV-i süsteemiülese aadressiraamatu sünkroonimist pole veel toimunud. Aga võis ka juhtuda viga. Palun käivita ta käsurealt ise käsuga „occ dav:sync-system-addressbook“.",
"DAV system address book size" : "DAV-i süsteemiülese aadressiraamatu suurus",
"The system address book is disabled" : "Süsteemiülene aadressiraamat pole kasutusel",
"The system address book is enabled, but contains more than the configured limit of %d contacts" : "Süsteemiülene aadressiraamat on kasutusel, kuid seal on andmeid rohkem, kui seadistatud %d kontakti ülempiir lubab",
"The system address book is enabled and contains less than the configured limit of %d contacts" : "Süsteemiülene aadressiraamat on kasutusel ning seal on andmeid vähem, kui seadistatud %d kontakti ülempiir lubab",
"WebDAV endpoint" : "WebDAV-i teenuse otspunkt",
"Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Ei õnnestunud kontrollida, kas sinu veebiserver on korrektselt seadistatud ja võimaldab kasutada failide sünkroniseerimist WebDAV-i vahendusel. Palun kontrolli seda käsitsi.",
"Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Sinu veebiserver pole veel failide sünkroniseerimiseks vajalikult seadistatud, kuna WebDAV liides paistab olevat katki.",
+10 -10
View File
@@ -252,16 +252,16 @@
"Due on %s" : "Tähtaeg: %s",
"Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Tere tulemast Nextcloudi Kalendrisse!\n\nSee näidissündmus võimaldab sul tutvuda Nextcloudi Kalendri paindlikkusega oma aja plaanimisel - proovi teha igasuguseid muudatusi!\n\nNextcloudi Kalendriga saad sa:\n- vaevata luua, muuta ja hallata sündmusi,\n- koostada mitmeid kalendreid ning neid jagada tiimikaaslaste, sõprade ja perega,\n- kontrollida teiste vabu aega ja enda omi näidata teistele,\n- kasutada sujuvat CalDAV-i põhist lõimingut teiste rakenduste ja seadmetega,\n- kohendada kõike oma vajadustele: ajastades korduvaid sündmusi ning sättida teavitusi ja muid seadistusi.",
"Example event - open me!" : "Näidissündmus - klõpsi mind!",
"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.",
"Enable System Address Book" : "Kasuta süsteemset aadressiraamatut",
"DAV system address book" : "DAV-i süsteemne aadressiraamat",
"No outstanding DAV system address book sync." : "Pole DAV-i süsteemse aadressiraamatu sünkroniseerimist.",
"The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Kuna selles serveris on üle 1000 kasutaja, siis DAV-i süsteemse aadressiraamatu sünkroonomist poel veel toimunud. Aga võis ka juhtuda viga. Palun käivita ta käsurealt ise käsuga „occ dav:sync-system-addressbook“.",
"DAV system address book size" : "DAV-i süsteemse aadressiraamatu suurus",
"The system address book is disabled" : "Süsteemne aadressiraamat pole kasutusel",
"The system address book is enabled, but contains more than the configured limit of %d contacts" : "Süsteemne aadressiraamat on kasutusel, kuid seal on andmeid rohkem, kui seadistatud %d kontakti ülempiir lubab",
"The system address book is enabled and contains less than the configured limit of %d contacts" : "Süsteemne aadressiraamat on kasutusel ning seal on andmeid vähem, kui seadistatud %d kontakti ülempiir lubab",
"System Address Book" : "Süsteemiülene aadressiraamat",
"The system address book contains contact information for all users in your instance." : "Süsteemiüleses aadressiraamatus leiduvad kõikide selle serveri kasutajate kontaktandmed.",
"Enable System Address Book" : "Kasuta süsteemiülest aadressiraamatut",
"DAV system address book" : "DAV-i süsteemiülene aadressiraamat",
"No outstanding DAV system address book sync." : "DAV-i süsteemiülese aadressiraamatu sünkroonimist pole ootel või toimunud.",
"The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Kuna selles serveris on üle 1000 kasutaja, siis DAV-i süsteemiülese aadressiraamatu sünkroonimist pole veel toimunud. Aga võis ka juhtuda viga. Palun käivita ta käsurealt ise käsuga „occ dav:sync-system-addressbook“.",
"DAV system address book size" : "DAV-i süsteemiülese aadressiraamatu suurus",
"The system address book is disabled" : "Süsteemiülene aadressiraamat pole kasutusel",
"The system address book is enabled, but contains more than the configured limit of %d contacts" : "Süsteemiülene aadressiraamat on kasutusel, kuid seal on andmeid rohkem, kui seadistatud %d kontakti ülempiir lubab",
"The system address book is enabled and contains less than the configured limit of %d contacts" : "Süsteemiülene aadressiraamat on kasutusel ning seal on andmeid vähem, kui seadistatud %d kontakti ülempiir lubab",
"WebDAV endpoint" : "WebDAV-i teenuse otspunkt",
"Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Ei õnnestunud kontrollida, kas sinu veebiserver on korrektselt seadistatud ja võimaldab kasutada failide sünkroniseerimist WebDAV-i vahendusel. Palun kontrolli seda käsitsi.",
"Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Sinu veebiserver pole veel failide sünkroniseerimiseks vajalikult seadistatud, kuna WebDAV liides paistab olevat katki.",
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Níorbh fhéidir méid an chomhaid a sheiceáil: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Níorbh fhéidir an comhad a oscailt: %1$s (%2$d), is cosúil go bhfuil an comhad ann",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Níorbh fhéidir an comhad a oscailt: %1$s (%2$d), is cosúil nach bhfuil an comhad ann.",
"Failed to get size for : %1$s" : "Theip ar mhéid a fháil le haghaidh: %1$s",
"Encryption not ready: %1$s" : "Níl an criptiúchán réidh: %1$s",
"Failed to open file: %1$s" : "Níorbh fhéidir an comhad a oscailt: %1$s",
"Failed to unlink: %1$s" : "Theip ar dhínascadh: %1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "Níorbh fhéidir méid an chomhaid a sheiceáil: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Níorbh fhéidir an comhad a oscailt: %1$s (%2$d), is cosúil go bhfuil an comhad ann",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Níorbh fhéidir an comhad a oscailt: %1$s (%2$d), is cosúil nach bhfuil an comhad ann.",
"Failed to get size for : %1$s" : "Theip ar mhéid a fháil le haghaidh: %1$s",
"Encryption not ready: %1$s" : "Níl an criptiúchán réidh: %1$s",
"Failed to open file: %1$s" : "Níorbh fhéidir an comhad a oscailt: %1$s",
"Failed to unlink: %1$s" : "Theip ar dhínascadh: %1$s",
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Produciuse un erro ao comprobar o tamaño do ficheiro: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Non foi posíbel abrir o ficheiro: %1$s (%2$d), semella o ficheiro existe",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Non foi posíbel abrir o ficheiro: %1$s (%2$d), semella o ficheiro non existe",
"Failed to get size for : %1$s" : "Produciuse un fallo ao obter o tamaño de: %1$s",
"Encryption not ready: %1$s" : "A cifraxe non está preparada: %1$s",
"Failed to open file: %1$s" : "Produciuse un erro ao abrir o ficheiro: %1$s",
"Failed to unlink: %1$s" : "Produciuse un erro ao desligar: %1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "Produciuse un erro ao comprobar o tamaño do ficheiro: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Non foi posíbel abrir o ficheiro: %1$s (%2$d), semella o ficheiro existe",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Non foi posíbel abrir o ficheiro: %1$s (%2$d), semella o ficheiro non existe",
"Failed to get size for : %1$s" : "Produciuse un fallo ao obter o tamaño de: %1$s",
"Encryption not ready: %1$s" : "A cifraxe non está preparada: %1$s",
"Failed to open file: %1$s" : "Produciuse un erro ao abrir o ficheiro: %1$s",
"Failed to unlink: %1$s" : "Produciuse un erro ao desligar: %1$s",
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Kon bestandsomvang niet controleren: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Kon bestand niet openen: %1$s (%2$d), bestand lijkt wel te bestaan",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Kon bestand niet openen: %1$s (%2$d), bestand lijkt niet te bestaan",
"Failed to get size for : %1$s" : "Niet gelukt om grootte te krijgen voor : %1$s",
"Encryption not ready: %1$s" : "Versleuteling niet gereed: %1$s",
"Failed to open file: %1$s" : "Kon het bestand %1$s niet openen",
"Failed to unlink: %1$s" : "Kon link niet verwijderen: %1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "Kon bestandsomvang niet controleren: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Kon bestand niet openen: %1$s (%2$d), bestand lijkt wel te bestaan",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Kon bestand niet openen: %1$s (%2$d), bestand lijkt niet te bestaan",
"Failed to get size for : %1$s" : "Niet gelukt om grootte te krijgen voor : %1$s",
"Encryption not ready: %1$s" : "Versleuteling niet gereed: %1$s",
"Failed to open file: %1$s" : "Kon het bestand %1$s niet openen",
"Failed to unlink: %1$s" : "Kon link niet verwijderen: %1$s",
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Falha ao verificar o tamanho do arquivo: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Não foi possível abrir o arquivo: %1$s (%2$d), o arquivo parece existir",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Não foi possível abrir o arquivo: %1$s (%2$d), o arquivo parece não existir",
"Failed to get size for : %1$s" : "Falha ao obter o tamanho para: %1$s",
"Encryption not ready: %1$s" : "A criptografia não está pronta: %1$s",
"Failed to open file: %1$s" : "Falha ao abrir arquivo: %1$s",
"Failed to unlink: %1$s" : "Falha ao desvincular: %1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "Falha ao verificar o tamanho do arquivo: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Não foi possível abrir o arquivo: %1$s (%2$d), o arquivo parece existir",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Não foi possível abrir o arquivo: %1$s (%2$d), o arquivo parece não existir",
"Failed to get size for : %1$s" : "Falha ao obter o tamanho para: %1$s",
"Encryption not ready: %1$s" : "A criptografia não está pronta: %1$s",
"Failed to open file: %1$s" : "Falha ao abrir arquivo: %1$s",
"Failed to unlink: %1$s" : "Falha ao desvincular: %1$s",
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "Dosya boyutu denetlenemedi: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Dosya açılamadı: %1$s (%2$d), dosya var gibi görünüyor",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Dosya açılamadı: %1$s (%2$d), dosya var gibi görünmüyor",
"Failed to get size for : %1$s" : "Dosya boyutu alınamadı: %1$s",
"Encryption not ready: %1$s" : "Şifreleme hazır değil: %1$s",
"Failed to open file: %1$s" : "Dosya açılamadı: %1$s",
"Failed to unlink: %1$s" : "Bağlantı kaldırılamadı: %1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "Dosya boyutu denetlenemedi: %1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "Dosya açılamadı: %1$s (%2$d), dosya var gibi görünüyor",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "Dosya açılamadı: %1$s (%2$d), dosya var gibi görünmüyor",
"Failed to get size for : %1$s" : "Dosya boyutu alınamadı: %1$s",
"Encryption not ready: %1$s" : "Şifreleme hazır değil: %1$s",
"Failed to open file: %1$s" : "Dosya açılamadı: %1$s",
"Failed to unlink: %1$s" : "Bağlantı kaldırılamadı: %1$s",
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "檢查檔案大小失敗:%1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "無法開啟檔案:%1$s%2$d),檔案似乎存在",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "無法開啟檔案:%1$s%2$d),檔案似乎不存在",
"Failed to get size for : %1$s" : "無法取得以下項目的大小:%1$s",
"Encryption not ready: %1$s" : "尚未準備好加密:%1$s",
"Failed to open file: %1$s" : "開啟檔案失敗:%1$s",
"Failed to unlink: %1$s" : "解除連結失敗:%1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "檢查檔案大小失敗:%1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "無法開啟檔案:%1$s%2$d),檔案似乎存在",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "無法開啟檔案:%1$s%2$d),檔案似乎不存在",
"Failed to get size for : %1$s" : "無法取得以下項目的大小:%1$s",
"Encryption not ready: %1$s" : "尚未準備好加密:%1$s",
"Failed to open file: %1$s" : "開啟檔案失敗:%1$s",
"Failed to unlink: %1$s" : "解除連結失敗:%1$s",
+1
View File
@@ -236,6 +236,7 @@ OC.L10N.register(
"Failed to check file size: %1$s" : "檢查檔案大小失敗:%1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "無法開啟檔案:%1$s (%2$d),檔案似乎存在",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "無法開啟檔案:%1$s%2$d),檔案似乎不存在",
"Failed to get size for : %1$s" : "無法取得以下項目的大小:%1$s",
"Encryption not ready: %1$s" : "尚未準備好加密:%1$s",
"Failed to open file: %1$s" : "開啟檔案失敗:%1$s",
"Failed to unlink: %1$s" : "解除連結失敗:%1$s",
+1
View File
@@ -234,6 +234,7 @@
"Failed to check file size: %1$s" : "檢查檔案大小失敗:%1$s",
"Could not open file: %1$s (%2$d), file does seem to exist" : "無法開啟檔案:%1$s (%2$d),檔案似乎存在",
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "無法開啟檔案:%1$s%2$d),檔案似乎不存在",
"Failed to get size for : %1$s" : "無法取得以下項目的大小:%1$s",
"Encryption not ready: %1$s" : "尚未準備好加密:%1$s",
"Failed to open file: %1$s" : "開啟檔案失敗:%1$s",
"Failed to unlink: %1$s" : "解除連結失敗:%1$s",
+6 -2
View File
@@ -1570,8 +1570,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
$stmt->execute([$calendarId, $objectUri, $calendarType]);
$qb = $this->db->getQueryBuilder();
$qb->delete('calendarobjects')
->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
->andWhere($qb->expr()->eq('uri', $qb->createNamedParameter($objectUri)))
->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
->executeStatement();
$this->purgeProperties($calendarId, $data['id']);
+2 -4
View File
@@ -43,9 +43,9 @@ class CalendarProvider implements ICalendarProvider {
});
}
$additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
$iCalendars = [];
$additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
foreach ($calendarInfos as $calendarInfo) {
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
@@ -60,9 +60,7 @@ class CalendarProvider implements ICalendarProvider {
);
}
$additionalFederatedProps = $this->getAdditionalPropertiesForCalendars(
$federatedCalendarInfos,
);
$additionalFederatedProps = $this->getAdditionalPropertiesForCalendars($federatedCalendarInfos);
foreach ($federatedCalendarInfos as $calendarInfo) {
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
@@ -104,9 +104,10 @@ class CalendarFederationProvider implements ICloudFederationProvider {
);
}
// TODO: implement read-write sharing
// convert access to permissions
$permissions = match ($access) {
DavSharingBackend::ACCESS_READ => Constants::PERMISSION_READ,
DavSharingBackend::ACCESS_READ_WRITE => Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE,
default => throw new ProviderCouldNotAddShareException(
"Unsupported access value: $access",
'',
@@ -122,20 +123,27 @@ class CalendarFederationProvider implements ICloudFederationProvider {
$sharedWithPrincipal = 'principals/users/' . $share->getShareWith();
// Delete existing incoming federated share first
$this->federatedCalendarMapper->deleteByUri($sharedWithPrincipal, $calendarUri);
$calendar = $this->federatedCalendarMapper->findByUri($sharedWithPrincipal, $calendarUri);
$calendar = new FederatedCalendarEntity();
$calendar->setPrincipaluri($sharedWithPrincipal);
$calendar->setUri($calendarUri);
$calendar->setRemoteUrl($calendarUrl);
$calendar->setDisplayName($displayName);
$calendar->setColor($color);
$calendar->setToken($share->getShareSecret());
$calendar->setSharedBy($share->getSharedBy());
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
$calendar->setPermissions($permissions);
$calendar->setComponents($components);
$calendar = $this->federatedCalendarMapper->insert($calendar);
if ($calendar === null) {
$calendar = new FederatedCalendarEntity();
$calendar->setPrincipaluri($sharedWithPrincipal);
$calendar->setUri($calendarUri);
$calendar->setRemoteUrl($calendarUrl);
$calendar->setDisplayName($displayName);
$calendar->setColor($color);
$calendar->setToken($share->getShareSecret());
$calendar->setSharedBy($share->getSharedBy());
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
$calendar->setPermissions($permissions);
$calendar->setComponents($components);
$calendar = $this->federatedCalendarMapper->insert($calendar);
} else {
$calendar->setToken($share->getShareSecret());
$calendar->setPermissions($permissions);
$calendar->setComponents($components);
$this->federatedCalendarMapper->update($calendar);
}
$this->jobList->add(FederatedCalendarSyncJob::class, [
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
@@ -10,29 +10,289 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\Federation;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCP\IConfig;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Backend;
use OCP\Constants;
use Sabre\CalDAV\ICalendar;
use Sabre\CalDAV\Plugin;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IMultiGet;
use Sabre\DAV\INode;
use Sabre\DAV\IProperties;
use Sabre\DAV\PropPatch;
class FederatedCalendar implements ICalendar, IProperties, IMultiGet {
private const CALENDAR_TYPE = CalDavBackend::CALENDAR_TYPE_FEDERATED;
private const DAV_PROPERTY_CALENDAR_LABEL = '{DAV:}displayname';
private const DAV_PROPERTY_CALENDAR_COLOR = '{http://apple.com/ns/ical/}calendar-color';
private string $principalUri;
private string $calendarUri;
private ?array $calendarACL = null;
private FederatedCalendarEntity $federationInfo;
class FederatedCalendar extends Calendar {
public function __construct(
Backend\BackendInterface $caldavBackend,
$calendarInfo,
IL10N $l10n,
IConfig $config,
LoggerInterface $logger,
private readonly FederatedCalendarMapper $federatedCalendarMapper,
private readonly FederatedCalendarSyncService $federatedCalendarService,
private readonly CalDavBackend $caldavBackend,
$calendarInfo,
) {
parent::__construct($caldavBackend, $calendarInfo, $l10n, $config, $logger);
$this->principalUri = $calendarInfo['principaluri'];
$this->calendarUri = $calendarInfo['uri'];
$this->federationInfo = $federatedCalendarMapper->findByUri($this->principalUri, $this->calendarUri);
}
public function delete() {
$this->federatedCalendarMapper->deleteById($this->getResourceId());
public function getResourceId(): int {
return $this->federationInfo->getId();
}
public function getName(): string {
return $this->federationInfo->getUri();
}
/**
* @param string $name Name of the file
*/
public function setName($name): void {
throw new MethodNotAllowed('Renaming federated calendars is not allowed');
}
protected function getCalendarType(): int {
return CalDavBackend::CALENDAR_TYPE_FEDERATED;
return self::CALENDAR_TYPE;
}
public function getPrincipalURI(): string {
return $this->federationInfo->getPrincipaluri();
}
public function getOwner(): ?string {
return $this->federationInfo->getSharedByPrincipal();
}
public function getGroup(): ?string {
return null;
}
/**
* @return array<array-key, mixed>
*/
public function getACL(): array {
if ($this->calendarACL !== null) {
return $this->calendarACL;
}
$permissions = $this->federationInfo->getPermissions();
// default permission
$acl = [
// read object permission
[
'privilege' => '{DAV:}read',
'principal' => $this->principalUri,
'protected' => true,
],
// read acl permission
[
'privilege' => '{DAV:}read-acl',
'principal' => $this->principalUri,
'protected' => true,
],
// write properties permission (calendar name, color)
[
'privilege' => '{DAV:}write-properties',
'principal' => $this->principalUri,
'protected' => true,
],
];
// create permission
if ($permissions & Constants::PERMISSION_CREATE) {
$acl[] = [
'privilege' => '{DAV:}bind',
'principal' => $this->principalUri,
'protected' => true,
];
}
// update permission
if ($permissions & Constants::PERMISSION_UPDATE) {
$acl[] = [
'privilege' => '{DAV:}write-content',
'principal' => $this->principalUri,
'protected' => true,
];
}
// delete permission
if ($permissions & Constants::PERMISSION_DELETE) {
$acl[] = [
'privilege' => '{DAV:}unbind',
'principal' => $this->principalUri,
'protected' => true,
];
}
// cache the calculated ACL for later use
$this->calendarACL = $acl;
return $acl;
}
public function setACL(array $acl): void {
throw new MethodNotAllowed('Changing ACLs on federated calendars is not allowed');
}
public function getSupportedPrivilegeSet(): ?array {
return null;
}
/**
* @return array<string, mixed> properties array, with property name as key
*/
public function getProperties($properties): array {
return [
self::DAV_PROPERTY_CALENDAR_LABEL => $this->federationInfo->getDisplayName(),
self::DAV_PROPERTY_CALENDAR_COLOR => $this->federationInfo->getColor(),
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(explode(',', $this->federationInfo->getComponents())),
];
}
public function propPatch(PropPatch $propPatch): void {
$mutations = $propPatch->getMutations();
if (count($mutations) > 0) {
// evaluate if name was changed
if (isset($mutations[self::DAV_PROPERTY_CALENDAR_LABEL])) {
$this->federationInfo->setDisplayName($mutations[self::DAV_PROPERTY_CALENDAR_LABEL]);
$propPatch->setResultCode(self::DAV_PROPERTY_CALENDAR_LABEL, 200);
}
// evaluate if color was changed
if (isset($mutations[self::DAV_PROPERTY_CALENDAR_COLOR])) {
$this->federationInfo->setColor($mutations[self::DAV_PROPERTY_CALENDAR_COLOR]);
$propPatch->setResultCode(self::DAV_PROPERTY_CALENDAR_COLOR, 200);
}
$this->federatedCalendarMapper->update($this->federationInfo);
}
}
public function getChildACL(): array {
return $this->getACL();
}
public function getLastModified(): ?int {
return $this->federationInfo->getLastSync();
}
public function delete(): void {
$this->federatedCalendarMapper->deleteById($this->getResourceId());
}
/**
* @param string $name Name of the file
*/
public function createDirectory($name): void {
throw new MethodNotAllowed('Creating nested collection is not allowed');
}
public function calendarQuery(array $filters): array {
$uris = $this->caldavBackend->calendarQuery($this->federationInfo->getId(), $filters, $this->getCalendarType());
return $uris;
}
/**
* @param string $name Name of the file
*/
public function getChild($name): INode {
$obj = $this->caldavBackend->getCalendarObject($this->federationInfo->getId(), $name, $this->getCalendarType());
if ($obj === null) {
throw new NotFound('Calendar object not found');
}
return new FederatedCalendarObject($this, $obj);
}
/**
* @return array<INode>
*/
public function getChildren(): array {
$objs = $this->caldavBackend->getCalendarObjects($this->federationInfo->getId(), $this->getCalendarType());
$children = [];
foreach ($objs as $obj) {
$children[] = new FederatedCalendarObject($this, $obj);
}
return $children;
}
/**
* @param array<string> $paths Names of the files
*
* @return array<INode>
*/
public function getMultipleChildren(array $paths): array {
$objs = $this->caldavBackend->getMultipleCalendarObjects($this->federationInfo->getId(), $paths, $this->getCalendarType());
$children = [];
foreach ($objs as $obj) {
$children[] = new FederatedCalendarObject($this, $obj);
}
return $children;
}
/**
* @param string $name Name of the file
*/
public function childExists($name): bool {
$obj = $this->caldavBackend->getCalendarObject($this->federationInfo->getId(), $name, $this->getCalendarType());
return $obj !== null;
}
/**
* @param string $name Name of the file
* @param resource|string $data Initial payload
*/
public function createFile($name, $data = null): string {
if (is_resource($data)) {
$data = stream_get_contents($data);
}
// Create on remote server first
$etag = $this->federatedCalendarService->createCalendarObject($this->federationInfo, $name, $data);
if (empty($etag)) {
throw new \Exception('Failed to create calendar object on remote server');
}
// Then store locally
return $this->caldavBackend->createCalendarObject($this->federationInfo->getId(), $name, $data, $this->getCalendarType());
}
/**
* @param string $name Name of the file
* @param resource|string $data Initial payload
*/
public function updateFile($name, $data): string {
if (is_resource($data)) {
$data = stream_get_contents($data);
}
// Update remote calendar first
$etag = $this->federatedCalendarService->updateCalendarObject($this->federationInfo, $name, $data);
if (empty($etag)) {
throw new \Exception('Failed to update calendar object on remote server');
}
// Then update locally
return $this->caldavBackend->updateCalendarObject($this->federationInfo->getId(), $name, $data, $this->getCalendarType());
}
public function deleteFile(string $name): void {
// Delete from remote server first
$this->federatedCalendarService->deleteCalendarObject($this->federationInfo, $name);
// Then delete locally
$this->caldavBackend->deleteCalendarObject($this->federationInfo->getId(), $name, $this->getCalendarType());
}
}
@@ -11,6 +11,7 @@ namespace OCA\DAV\CalDAV\Federation;
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
use OCP\AppFramework\Db\Entity;
use OCP\Constants;
use OCP\DB\Types;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
@@ -94,8 +95,8 @@ class FederatedCalendarEntity extends Entity {
'{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $this->getSyncTokenForSabre(),
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => $this->getSupportedCalendarComponentSet(),
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->getSharedByPrincipal(),
// TODO: implement read-write sharing
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => 1
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => ($this->getPermissions() & Constants::PERMISSION_UPDATE) === 0 ? 1 : 0,
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}permissions' => $this->getPermissions(),
];
}
}
@@ -9,34 +9,23 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\Federation;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\CalDAV\CalDavBackend;
use OCP\IConfig;
use OCP\IL10N;
use OCP\L10N\IFactory as IL10NFactory;
use Psr\Log\LoggerInterface;
class FederatedCalendarFactory {
private readonly IL10N $l10n;
public function __construct(
private readonly CalDavBackend $caldavBackend,
private readonly IConfig $config,
private readonly LoggerInterface $logger,
private readonly FederatedCalendarMapper $federatedCalendarMapper,
IL10NFactory $l10nFactory,
private readonly FederatedCalendarSyncService $federatedCalendarService,
private readonly CalDavBackend $caldavBackend,
) {
$this->l10n = $l10nFactory->get(Application::APP_ID);
}
public function createFederatedCalendar(array $calendarInfo): FederatedCalendar {
return new FederatedCalendar(
$this->federatedCalendarMapper,
$this->federatedCalendarService,
$this->caldavBackend,
$calendarInfo,
$this->l10n,
$this->config,
$this->logger,
$this->federatedCalendarMapper,
);
}
}
@@ -51,8 +51,7 @@ class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, ICalendarIs
}
public function getPermissions(): int {
// TODO: implement read-write sharing
return Constants::PERMISSION_READ;
return $this->calendarInfo['{http://owncloud.org/ns}permissions'] ?? Constants::PERMISSION_READ;
}
public function isDeleted(): bool {
@@ -64,7 +63,8 @@ class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, ICalendarIs
}
public function isWritable(): bool {
return false;
$permissions = $this->getPermissions();
return ($permissions & Constants::PERMISSION_UPDATE) !== 0;
}
public function isEnabled(): bool {
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Federation;
use Sabre\CalDAV\ICalendarObject;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAVACL\IACL;
class FederatedCalendarObject implements ICalendarObject, IACL {
public function __construct(
protected FederatedCalendar $calendarObject,
protected $objectData,
) {
}
public function getName(): string {
return $this->objectData['uri'];
}
/**
* @param string $name Name of the file
*/
public function setName($name) {
throw new \Exception('Not implemented');
}
public function get(): string {
return $this->objectData['calendardata'];
}
/**
* @param resource|string $data contents of the file
*/
public function put($data): string {
$etag = $this->calendarObject->updateFile($this->objectData['uri'], $data);
$this->objectData['calendardata'] = $data;
$this->objectData['etag'] = $etag;
return $etag;
}
public function delete(): void {
$this->calendarObject->deleteFile($this->objectData['uri']);
}
public function getContentType(): ?string {
$mime = 'text/calendar; charset=utf-8';
if (isset($this->objectData['component']) && $this->objectData['component']) {
$mime .= '; component=' . $this->objectData['component'];
}
return $mime;
}
public function getETag(): string {
if (isset($this->objectData['etag'])) {
return $this->objectData['etag'];
} else {
return '"' . md5($this->get()) . '"';
}
}
public function getLastModified(): int {
return $this->objectData['lastmodified'];
}
public function getSize(): int {
if (isset($this->objectData['size'])) {
return $this->objectData['size'];
} else {
return strlen($this->get());
}
}
public function getOwner(): ?string {
return $this->calendarObject->getPrincipalURI();
}
public function getGroup(): ?string {
return null;
}
/**
* @return array<array-key, mixed>
*/
public function getACL(): array {
return $this->calendarObject->getACL();
}
public function setACL(array $acl): void {
throw new MethodNotAllowed('Changing ACLs on federated events is not allowed');
}
public function getSupportedPrivilegeSet(): ?array {
return null;
}
}
@@ -9,20 +9,52 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\Federation;
use OCA\DAV\CalDAV\SyncService as CalDavSyncService;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Service\ASyncService;
use OCP\AppFramework\Db\TTransactional;
use OCP\AppFramework\Http;
use OCP\Federation\ICloudIdManager;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IDBConnection;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Log\LoggerInterface;
class FederatedCalendarSyncService {
class FederatedCalendarSyncService extends ASyncService {
use TTransactional;
private const SYNC_TOKEN_PREFIX = 'http://sabre.io/ns/sync/';
public function __construct(
IClientService $clientService,
IConfig $config,
private readonly FederatedCalendarMapper $federatedCalendarMapper,
private readonly LoggerInterface $logger,
private readonly CalDavSyncService $syncService,
private readonly CalDavBackend $backend,
private readonly IDBConnection $dbConnection,
private readonly ICloudIdManager $cloudIdManager,
) {
parent::__construct($clientService, $config);
}
/**
* Extract and encode credentials from a federated calendar entity.
*
* @return array{username: string, remoteUrl: string, token: string}
*/
private function getCredentials(FederatedCalendarEntity $calendar): array {
[,, $sharedWith] = explode('/', $calendar->getPrincipaluri());
$calDavUser = $this->cloudIdManager->getCloudId($sharedWith, null)->getId();
// Need to encode the cloud id as it might contain a colon which is not allowed in basic
// auth according to RFC 7617
$calDavUser = base64_encode($calDavUser);
return [
'username' => $calDavUser,
'remoteUrl' => $calendar->getRemoteUrl(),
'token' => $calendar->getToken(),
];
}
/**
@@ -31,29 +63,77 @@ class FederatedCalendarSyncService {
* @throws ClientExceptionInterface If syncing the calendar fails.
*/
public function syncOne(FederatedCalendarEntity $calendar): int {
[,, $sharedWith] = explode('/', $calendar->getPrincipaluri());
$calDavUser = $this->cloudIdManager->getCloudId($sharedWith, null)->getId();
$remoteUrl = $calendar->getRemoteUrl();
$credentials = $this->getCredentials($calendar);
$syncToken = $calendar->getSyncTokenForSabre();
// Need to encode the cloud id as it might contain a colon which is not allowed in basic
// auth according to RFC 7617
$calDavUser = base64_encode($calDavUser);
try {
$response = $this->requestSyncReport(
$credentials['remoteUrl'],
$credentials['username'],
$credentials['token'],
$syncToken,
);
} catch (ClientExceptionInterface $ex) {
if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
// Remote server revoked access to the calendar => remove it
$this->federatedCalendarMapper->delete($calendar);
$this->logger->warning("Authorization failed, remove federated calendar: {$credentials['remoteUrl']}", [
'app' => 'dav',
]);
return 0;
}
$this->logger->error('Client exception:', ['app' => 'dav', 'exception' => $ex]);
throw $ex;
}
$syncResponse = $this->syncService->syncRemoteCalendar(
$remoteUrl,
$calDavUser,
$calendar->getToken(),
$syncToken,
$calendar,
);
// Process changes from remote
$downloadedEvents = 0;
foreach ($response['response'] as $resource => $status) {
$objectUri = basename($resource);
if (isset($status[200])) {
// Object created or updated
$absoluteUrl = $this->prepareUri($credentials['remoteUrl'], $resource);
$calendarData = $this->download($absoluteUrl, $credentials['username'], $credentials['token']);
$this->atomic(function () use ($calendar, $objectUri, $calendarData): void {
$existingObject = $this->backend->getCalendarObject(
$calendar->getId(),
$objectUri,
CalDavBackend::CALENDAR_TYPE_FEDERATED
);
if (!$existingObject) {
$this->backend->createCalendarObject(
$calendar->getId(),
$objectUri,
$calendarData,
CalDavBackend::CALENDAR_TYPE_FEDERATED
);
} else {
$this->backend->updateCalendarObject(
$calendar->getId(),
$objectUri,
$calendarData,
CalDavBackend::CALENDAR_TYPE_FEDERATED
);
}
}, $this->dbConnection);
$downloadedEvents++;
} else {
// Object deleted
$this->backend->deleteCalendarObject(
$calendar->getId(),
$objectUri,
CalDavBackend::CALENDAR_TYPE_FEDERATED,
true
);
}
}
$newSyncToken = $syncResponse->getSyncToken();
$newSyncToken = $response['token'];
// Check sync token format and extract the actual sync token integer
$matches = [];
if (!preg_match('/^http:\/\/sabre\.io\/ns\/sync\/([0-9]+)$/', $newSyncToken, $matches)) {
$this->logger->error("Failed to sync federated calendar at $remoteUrl: New sync token has unexpected format: $newSyncToken", [
$this->logger->error("Failed to sync federated calendar at {$credentials['remoteUrl']}: New sync token has unexpected format: $newSyncToken", [
'calendar' => $calendar->toCalendarInfo(),
'newSyncToken' => $newSyncToken,
]);
@@ -67,10 +147,58 @@ class FederatedCalendarSyncService {
$newSyncToken,
);
} else {
$this->logger->debug("Sync Token for $remoteUrl unchanged from previous sync");
$this->logger->debug("Sync Token for {$credentials['remoteUrl']} unchanged from previous sync");
$this->federatedCalendarMapper->updateSyncTime($calendar->getId());
}
return $syncResponse->getDownloadedEvents();
return $downloadedEvents;
}
/**
* Create a calendar object on the remote server.
*
* @throws ClientExceptionInterface If the remote request fails.
*/
public function createCalendarObject(FederatedCalendarEntity $calendar, string $name, string $data): string {
$credentials = $this->getCredentials($calendar);
$objectUrl = $this->prepareUri($credentials['remoteUrl'], $name);
return $this->requestPut(
$objectUrl,
$credentials['username'],
$credentials['token'],
$data,
'text/calendar; charset=utf-8'
);
}
/**
* Update a calendar object on the remote server.
*
* @throws ClientExceptionInterface If the remote request fails.
*/
public function updateCalendarObject(FederatedCalendarEntity $calendar, string $name, string $data): string {
$credentials = $this->getCredentials($calendar);
$objectUrl = $this->prepareUri($credentials['remoteUrl'], $name);
return $this->requestPut(
$objectUrl,
$credentials['username'],
$credentials['token'],
$data,
'text/calendar; charset=utf-8'
);
}
/**
* Delete a calendar object on the remote server.
*
* @throws ClientExceptionInterface If the remote request fails.
*/
public function deleteCalendarObject(FederatedCalendarEntity $calendar, string $name): void {
$credentials = $this->getCredentials($calendar);
$objectUrl = $this->prepareUri($credentials['remoteUrl'], $name);
$this->requestDelete($objectUrl, $credentials['username'], $credentials['token']);
}
}
+9 -1
View File
@@ -12,6 +12,7 @@ use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\CalDAV\CalendarObject;
use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\CalDAV\Federation\FederatedCalendar;
use OCA\DAV\CalDAV\TipBroker;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
@@ -173,8 +174,15 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
return;
}
/** @var Calendar $calendarNode */
/** @var Calendar&ICalendar $calendarNode */
$calendarNode = $this->server->tree->getNodeForPath($calendarPath);
// abort if calendar is federated
if ($calendarNode instanceof FederatedCalendar) {
$this->logger->debug('Not processing scheduling for federated calendar at path: ' . $calendarPath);
return;
}
// extract addresses for owner
$addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
// determine if request is from a sharee
+3 -1
View File
@@ -20,13 +20,15 @@ class Capabilities implements ICapability {
}
/**
* @return array{dav: array{chunking: string, public_shares_chunking: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}}
* @return array{dav: array{chunking: string, public_shares_chunking: bool, search_supports_creation_time: bool, search_supports_upload_time: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}}
*/
public function getCapabilities() {
$capabilities = [
'dav' => [
'chunking' => '1.0',
'public_shares_chunking' => true,
'search_supports_creation_time' => true,
'search_supports_upload_time' => true,
]
];
if ($this->config->getSystemValueBool('bulkupload.enabled', true)) {
+2 -1
View File
@@ -22,6 +22,7 @@ use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Security\ISecureRandom;
use OCP\Server;
use Psr\Log\LoggerInterface;
@@ -66,7 +67,7 @@ class CreateCalendar extends Command {
Server::get(ProxyMapper::class),
Server::get(KnownUserService::class),
Server::get(IConfig::class),
\OC::$server->getL10NFactory(),
Server::get(IFactory::class),
);
$random = Server::get(ISecureRandom::class);
$logger = Server::get(LoggerInterface::class);
+1 -1
View File
@@ -532,7 +532,7 @@ class Directory extends Node implements
}
if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
$node = new 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()) {
+6 -2
View File
@@ -480,11 +480,15 @@ class File extends Node implements IFile {
}
}
$logger = Server::get(LoggerInterface::class);
// comparing current file size with the one in DB
// if different, fix DB and refresh cache.
//
$fsSize = $this->fileView->filesize($this->getPath());
if ($this->getSize() !== $fsSize) {
$logger = Server::get(LoggerInterface::class);
if ($fsSize === false) {
$logger->warning('file not found on storage after successfully opening it');
throw new ServiceUnavailable($this->l10n->t('Failed to get size for : %1$s', [$this->getPath()]));
} elseif ($this->getSize() !== $fsSize) {
$logger->warning('fixing cached size of file id=' . $this->getId() . ', cached size was ' . $this->getSize() . ', but the filesystem reported a size of ' . $fsSize);
$this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
+5
View File
@@ -86,6 +86,7 @@ class FileSearchBackend implements ISearchBackend {
new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition('{DAV:}creationdate', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition('{http://nextcloud.org/ns}upload_time', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
@@ -299,6 +300,8 @@ class FileSearchBackend implements ISearchBackend {
return $node->getName();
case '{DAV:}getlastmodified':
return $node->getLastModified();
case '{DAV:}creationdate':
return $node->getNode()->getCreationTime();
case '{http://nextcloud.org/ns}upload_time':
return $node->getNode()->getUploadTime();
case FilesPlugin::SIZE_PROPERTYNAME:
@@ -461,6 +464,8 @@ class FileSearchBackend implements ISearchBackend {
return 'mimetype';
case '{DAV:}getlastmodified':
return 'mtime';
case '{DAV:}creationdate':
return 'creation_time';
case '{http://nextcloud.org/ns}upload_time':
return 'upload_time';
case FilesPlugin::SIZE_PROPERTYNAME:
+2 -1
View File
@@ -8,6 +8,7 @@
namespace OCA\DAV\Files;
use OCP\Files\FileInfo;
use OCP\Files\IRootFolder;
use OCP\IUserSession;
use OCP\Server;
use Sabre\DAV\INode;
@@ -35,7 +36,7 @@ class RootCollection extends AbstractPrincipalCollection {
// in the future this could be considered to be used for accessing shared files
return new SimpleCollection($name);
}
$userFolder = \OC::$server->getUserFolder();
$userFolder = Server::get(IRootFolder::class)->getUserFolder($user->getUID());
if (!($userFolder instanceof FileInfo)) {
throw new \Exception('Home does not exist');
}
+2 -1
View File
@@ -42,6 +42,7 @@ use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Security\ISecureRandom;
use OCP\Server;
use OCP\SystemTag\ISystemTagManager;
@@ -75,7 +76,7 @@ class RootCollection extends SimpleCollection {
$proxyMapper,
Server::get(KnownUserService::class),
Server::get(IConfig::class),
\OC::$server->getL10NFactory()
Server::get(IFactory::class)
);
$groupPrincipalBackend = new GroupPrincipalBackend($groupManager, $userSession, $shareManager, $config);
+21 -14
View File
@@ -150,13 +150,13 @@ class EventsSearchProvider extends ACalendarSearchProvider implements IFiltering
$formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById): SearchResultEntry {
$component = $this->getPrimaryComponent($eventRow['calendardata'], self::$componentType);
$title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled event'));
$subline = $this->generateSubline($component);
if ($eventRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) {
$calendar = $calendarsById[$eventRow['calendarid']];
} else {
$calendar = $subscriptionsById[$eventRow['calendarid']];
}
$subline = $this->generateSubline($component, $calendar);
$resourceUrl = $this->getDeepLinkToCalendarApp($calendar['principaluri'], $calendar['uri'], $eventRow['uri']);
$result = new SearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false);
@@ -204,7 +204,7 @@ class EventsSearchProvider extends ACalendarSearchProvider implements IFiltering
. $calendarObjectUri;
}
protected function generateSubline(Component $eventComponent): string {
protected function generateSubline(Component $eventComponent, array $calendarInfo): string {
$dtStart = $eventComponent->DTSTART;
$dtEnd = $this->getDTEndForEvent($eventComponent);
$isAllDayEvent = $dtStart instanceof Property\ICalendar\Date;
@@ -214,24 +214,31 @@ class EventsSearchProvider extends ACalendarSearchProvider implements IFiltering
if ($isAllDayEvent) {
$endDateTime->modify('-1 day');
if ($this->isDayEqual($startDateTime, $endDateTime)) {
return $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
$formattedSubline = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
} else {
$formattedStart = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
$formattedEnd = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
$formattedSubline = "$formattedStart - $formattedEnd";
}
} else {
$formattedStartDate = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
$formattedEndDate = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
$formattedStartTime = $this->l10n->l('time', $startDateTime, ['width' => 'short']);
$formattedEndTime = $this->l10n->l('time', $endDateTime, ['width' => 'short']);
$formattedStart = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
$formattedEnd = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
return "$formattedStart - $formattedEnd";
if ($this->isDayEqual($startDateTime, $endDateTime)) {
$formattedSubline = "$formattedStartDate $formattedStartTime - $formattedEndTime";
} else {
$formattedSubline = "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime";
}
}
$formattedStartDate = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
$formattedEndDate = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
$formattedStartTime = $this->l10n->l('time', $startDateTime, ['width' => 'short']);
$formattedEndTime = $this->l10n->l('time', $endDateTime, ['width' => 'short']);
if ($this->isDayEqual($startDateTime, $endDateTime)) {
return "$formattedStartDate $formattedStartTime - $formattedEndTime";
if (isset($calendarInfo['{DAV:}displayname']) && !empty($calendarInfo['{DAV:}displayname'])) {
$formattedSubline = $formattedSubline . " ({$calendarInfo['{DAV:}displayname']})";
}
return "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime";
// string cast is just to make psalm happy
return (string)$formattedSubline;
}
protected function getDTEndForEvent(Component $eventComponent):Property {
+14 -10
View File
@@ -96,13 +96,13 @@ class TasksSearchProvider extends ACalendarSearchProvider {
$formattedResults = \array_map(function (array $taskRow) use ($calendarsById, $subscriptionsById):SearchResultEntry {
$component = $this->getPrimaryComponent($taskRow['calendardata'], self::$componentType);
$title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled task'));
$subline = $this->generateSubline($component);
if ($taskRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) {
$calendar = $calendarsById[$taskRow['calendarid']];
} else {
$calendar = $subscriptionsById[$taskRow['calendarid']];
}
$subline = $this->generateSubline($component, $calendar);
$resourceUrl = $this->getDeepLinkToTasksApp($calendar['uri'], $taskRow['uri']);
return new SearchResultEntry('', $title, $subline, $resourceUrl, 'icon-checkmark', false);
@@ -128,25 +128,29 @@ class TasksSearchProvider extends ACalendarSearchProvider {
);
}
protected function generateSubline(Component $taskComponent): string {
protected function generateSubline(Component $taskComponent, array $calendarInfo): string {
if ($taskComponent->COMPLETED) {
$completedDateTime = new \DateTime($taskComponent->COMPLETED->getDateTime()->format(\DateTimeInterface::ATOM));
$formattedDate = $this->l10n->l('date', $completedDateTime, ['width' => 'medium']);
return $this->l10n->t('Completed on %s', [$formattedDate]);
}
if ($taskComponent->DUE) {
$formattedSubline = $this->l10n->t('Completed on %s', [$formattedDate]);
} elseif ($taskComponent->DUE) {
$dueDateTime = new \DateTime($taskComponent->DUE->getDateTime()->format(\DateTimeInterface::ATOM));
$formattedDate = $this->l10n->l('date', $dueDateTime, ['width' => 'medium']);
if ($taskComponent->DUE->hasTime()) {
$formattedTime = $this->l10n->l('time', $dueDateTime, ['width' => 'short']);
return $this->l10n->t('Due on %s by %s', [$formattedDate, $formattedTime]);
$formattedSubline = $this->l10n->t('Due on %s by %s', [$formattedDate, $formattedTime]);
} else {
$formattedSubline = $this->l10n->t('Due on %s', [$formattedDate]);
}
return $this->l10n->t('Due on %s', [$formattedDate]);
} else {
$formattedSubline = '';
}
return '';
if (isset($calendarInfo['{DAV:}displayname']) && !empty($calendarInfo['{DAV:}displayname'])) {
$formattedSubline = $formattedSubline . (!empty($formattedSubline) ? ' ' : '') . "({$calendarInfo['{DAV:}displayname']})";
}
return $formattedSubline;
}
}
+66
View File
@@ -191,4 +191,70 @@ abstract class ASyncService {
rtrim($responseUri, '/'),
);
}
/**
* Push data to the remote server via HTTP PUT.
* Used for creating or updating CalDAV/CardDAV objects.
*
* @param string $url The absolute URL to PUT to
* @param string $username The username for authentication
* @param string $token The authentication token/password
* @param string $data The data to upload
* @param string $contentType The Content-Type header (e.g., 'text/calendar' or 'text/vcard')
*
* @return string The ETag returned by the server
*/
protected function requestPut(
string $url,
string $username,
string $token,
string $data,
string $contentType = 'text/calendar; charset=utf-8',
): string {
$client = $this->getClient();
$options = [
'auth' => [$username, $token],
'body' => $data,
'headers' => [
'Content-Type' => $contentType,
],
'verify' => !$this->config->getSystemValue(
'sharing.federation.allowSelfSignedCertificates',
false,
),
];
$response = $client->put($url, $options);
// Extract and return the ETag from the response
$etag = $response->getHeader('ETag');
return $etag;
}
/**
* Delete a resource from the remote server via HTTP DELETE.
* Used for deleting CalDAV/CardDAV objects.
*
* @param string $url The absolute URL to DELETE
* @param string $username The username for authentication
* @param string $token The authentication token/password
*/
protected function requestDelete(
string $url,
string $username,
string $token,
): void {
$client = $this->getClient();
$options = [
'auth' => [$username, $token],
'verify' => !$this->config->getSystemValue(
'sharing.federation.allowSelfSignedCertificates',
false,
),
];
$client->delete($url, $options);
}
}
@@ -15,6 +15,7 @@ use OCP\AppFramework\Services\IAppConfig;
use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
use Symfony\Component\Uid\Uuid;
@@ -26,6 +27,7 @@ class ExampleContactService {
private readonly IAppConfig $appConfig,
private readonly LoggerInterface $logger,
private readonly CardDavBackend $cardDav,
private readonly IL10N $l,
) {
$this->appData = $appDataFactory->get(Application::APP_ID);
}
@@ -131,6 +133,9 @@ class ExampleContactService {
} else {
$vcard->add('REV', $newRev);
}
if (!$vcard->Note) {
$vcard->add('note', $this->l->t('This is an example contact'));
}
// Level 3 means that the document is invalid
// https://sabre.io/vobject/vcard/#validating-vcard
+9 -1
View File
@@ -30,7 +30,9 @@
"type": "object",
"required": [
"chunking",
"public_shares_chunking"
"public_shares_chunking",
"search_supports_creation_time",
"search_supports_upload_time"
],
"properties": {
"chunking": {
@@ -39,6 +41,12 @@
"public_shares_chunking": {
"type": "boolean"
},
"search_supports_creation_time": {
"type": "boolean"
},
"search_supports_upload_time": {
"type": "boolean"
},
"bulkupload": {
"type": "string"
},
@@ -92,11 +92,12 @@ class CalendarFederationProviderTest extends TestCase {
->willReturn(true);
$this->federatedCalendarMapper->expects(self::once())
->method('deleteByUri')
->method('findByUri')
->with(
'principals/users/sharee1',
'ae4b8ab904076fff2b955ea21b1a0d92',
);
)
->willReturn(null);
$this->federatedCalendarMapper->expects(self::once())
->method('insert')
@@ -123,6 +124,68 @@ class CalendarFederationProviderTest extends TestCase {
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
}
public function testShareReceivedWithExistingCalendar(): void {
$share = $this->createMock(ICloudFederationShare::class);
$share->method('getShareType')
->willReturn('user');
$share->method('getProtocol')
->willReturn([
'version' => 'v1',
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
'displayName' => 'Calendar 1',
'color' => '#ff0000',
'access' => 3,
'components' => 'VEVENT,VTODO',
]);
$share->method('getShareWith')
->willReturn('sharee1');
$share->method('getShareSecret')
->willReturn('new-token');
$share->method('getSharedBy')
->willReturn('user1@nextcloud.remote');
$share->method('getSharedByDisplayName')
->willReturn('User 1');
$this->calendarFederationConfig->expects(self::once())
->method('isFederationEnabled')
->willReturn(true);
$existingCalendar = new FederatedCalendarEntity();
$existingCalendar->setId(10);
$existingCalendar->setPrincipaluri('principals/users/sharee1');
$existingCalendar->setUri('ae4b8ab904076fff2b955ea21b1a0d92');
$existingCalendar->setRemoteUrl('https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1');
$existingCalendar->setToken('old-token');
$existingCalendar->setPermissions(1);
$existingCalendar->setComponents('VEVENT');
$this->federatedCalendarMapper->expects(self::once())
->method('findByUri')
->with(
'principals/users/sharee1',
'ae4b8ab904076fff2b955ea21b1a0d92',
)
->willReturn($existingCalendar);
$this->federatedCalendarMapper->expects(self::never())
->method('insert');
$this->federatedCalendarMapper->expects(self::once())
->method('update')
->willReturnCallback(function (FederatedCalendarEntity $calendar) {
$this->assertEquals('new-token', $calendar->getToken());
$this->assertEquals(1, $calendar->getPermissions());
$this->assertEquals('VEVENT,VTODO', $calendar->getComponents());
return $calendar;
});
$this->jobList->expects(self::once())
->method('add')
->with(FederatedCalendarSyncJob::class, ['id' => 10]);
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
}
public function testShareReceivedWithInvalidProtocolVersion(): void {
$share = $this->createMock(ICloudFederationShare::class);
$share->method('getShareType')
@@ -270,7 +333,7 @@ class CalendarFederationProviderTest extends TestCase {
$this->calendarFederationProvider->shareReceived($share);
}
public function testShareReceivedWithUnsupportedAccess(): void {
public function testShareReceivedWithReadWriteAccess(): void {
$share = $this->createMock(ICloudFederationShare::class);
$share->method('getShareType')
->willReturn('user');
@@ -296,6 +359,65 @@ class CalendarFederationProviderTest extends TestCase {
->method('isFederationEnabled')
->willReturn(true);
$this->federatedCalendarMapper->expects(self::once())
->method('findByUri')
->with(
'principals/users/sharee1',
'ae4b8ab904076fff2b955ea21b1a0d92',
)
->willReturn(null);
$this->federatedCalendarMapper->expects(self::once())
->method('insert')
->willReturnCallback(function (FederatedCalendarEntity $calendar) {
$this->assertEquals('principals/users/sharee1', $calendar->getPrincipaluri());
$this->assertEquals('ae4b8ab904076fff2b955ea21b1a0d92', $calendar->getUri());
$this->assertEquals('https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1', $calendar->getRemoteUrl());
$this->assertEquals('Calendar 1', $calendar->getDisplayName());
$this->assertEquals('#ff0000', $calendar->getColor());
$this->assertEquals('token', $calendar->getToken());
$this->assertEquals('user1@nextcloud.remote', $calendar->getSharedBy());
$this->assertEquals('User 1', $calendar->getSharedByDisplayName());
$this->assertEquals(15, $calendar->getPermissions()); // READ | CREATE | UPDATE | DELETE
$this->assertEquals('VEVENT,VTODO', $calendar->getComponents());
$calendar->setId(10);
return $calendar;
});
$this->jobList->expects(self::once())
->method('add')
->with(FederatedCalendarSyncJob::class, ['id' => 10]);
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
}
public function testShareReceivedWithUnsupportedAccess(): void {
$share = $this->createMock(ICloudFederationShare::class);
$share->method('getShareType')
->willReturn('user');
$share->method('getProtocol')
->willReturn([
'version' => 'v1',
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
'displayName' => 'Calendar 1',
'color' => '#ff0000',
'access' => 999, // Invalid access value
'components' => 'VEVENT,VTODO',
]);
$share->method('getShareWith')
->willReturn('sharee1');
$share->method('getShareSecret')
->willReturn('token');
$share->method('getSharedBy')
->willReturn('user1@nextcloud.remote');
$share->method('getSharedByDisplayName')
->willReturn('User 1');
$this->calendarFederationConfig->expects(self::once())
->method('isFederationEnabled')
->willReturn(true);
$this->federatedCalendarMapper->expects(self::never())
->method('insert');
$this->jobList->expects(self::never())
@@ -9,13 +9,17 @@ declare(strict_types=1);
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
use OCA\DAV\CalDAV\SyncService as CalDavSyncService;
use OCA\DAV\CalDAV\SyncServiceResult;
use OCP\Federation\ICloudId;
use OCP\Federation\ICloudIdManager;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use OCP\IConfig;
use OCP\IDBConnection;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
@@ -26,21 +30,30 @@ class FederatedCalendarSyncServiceTest extends TestCase {
private FederatedCalendarMapper&MockObject $federatedCalendarMapper;
private LoggerInterface&MockObject $logger;
private CalDavSyncService&MockObject $calDavSyncService;
private CalDavBackend&MockObject $backend;
private IDBConnection&MockObject $dbConnection;
private ICloudIdManager&MockObject $cloudIdManager;
private IClientService&MockObject $clientService;
private IConfig&MockObject $config;
protected function setUp(): void {
parent::setUp();
$this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->calDavSyncService = $this->createMock(CalDavSyncService::class);
$this->backend = $this->createMock(CalDavBackend::class);
$this->dbConnection = $this->createMock(IDBConnection::class);
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
$this->clientService = $this->createMock(IClientService::class);
$this->config = $this->createMock(IConfig::class);
$this->federatedCalendarSyncService = new FederatedCalendarSyncService(
$this->clientService,
$this->config,
$this->federatedCalendarMapper,
$this->logger,
$this->calDavSyncService,
$this->backend,
$this->dbConnection,
$this->cloudIdManager,
);
}
@@ -61,16 +74,24 @@ class FederatedCalendarSyncServiceTest extends TestCase {
->with('user1')
->willReturn($cloudId);
$this->calDavSyncService->expects(self::once())
->method('syncRemoteCalendar')
->with(
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
'token',
'http://sabre.io/ns/sync/100',
$calendar,
)
->willReturn(new SyncServiceResult('http://sabre.io/ns/sync/101', 10));
// Mock HTTP client for sync report
$client = $this->createMock(IClient::class);
$response = $this->createMock(IResponse::class);
$response->method('getBody')
->willReturn('<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"><d:sync-token>http://sabre.io/ns/sync/101</d:sync-token></d:multistatus>');
$client->expects(self::once())
->method('request')
->with('REPORT', 'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2', self::anything())
->willReturn($response);
$this->clientService->method('newClient')
->willReturn($client);
$this->config->method('getSystemValueInt')
->willReturn(30);
$this->config->method('getSystemValue')
->willReturn(false);
$this->federatedCalendarMapper->expects(self::once())
->method('updateSyncTokenAndTime')
@@ -78,7 +99,7 @@ class FederatedCalendarSyncServiceTest extends TestCase {
$this->federatedCalendarMapper->expects(self::never())
->method('updateSyncTime');
$this->assertEquals(10, $this->federatedCalendarSyncService->syncOne($calendar));
$this->assertEquals(0, $this->federatedCalendarSyncService->syncOne($calendar));
}
public function testSyncOneUnchanged(): void {
@@ -97,16 +118,24 @@ class FederatedCalendarSyncServiceTest extends TestCase {
->with('user1')
->willReturn($cloudId);
$this->calDavSyncService->expects(self::once())
->method('syncRemoteCalendar')
->with(
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
'token',
'http://sabre.io/ns/sync/100',
$calendar,
)
->willReturn(new SyncServiceResult('http://sabre.io/ns/sync/100', 0));
// Mock HTTP client for sync report
$client = $this->createMock(IClient::class);
$response = $this->createMock(IResponse::class);
$response->method('getBody')
->willReturn('<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"><d:sync-token>http://sabre.io/ns/sync/100</d:sync-token></d:multistatus>');
$client->expects(self::once())
->method('request')
->with('REPORT', 'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2', self::anything())
->willReturn($response);
$this->clientService->method('newClient')
->willReturn($client);
$this->config->method('getSystemValueInt')
->willReturn(30);
$this->config->method('getSystemValue')
->willReturn(false);
$this->federatedCalendarMapper->expects(self::never())
->method('updateSyncTokenAndTime');
@@ -143,16 +172,24 @@ class FederatedCalendarSyncServiceTest extends TestCase {
->with('user1')
->willReturn($cloudId);
$this->calDavSyncService->expects(self::once())
->method('syncRemoteCalendar')
->with(
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
'token',
'http://sabre.io/ns/sync/100',
$calendar,
)
->willReturn(new SyncServiceResult($syncToken, 10));
// Mock HTTP client for sync report with unexpected token format
$client = $this->createMock(IClient::class);
$response = $this->createMock(IResponse::class);
$response->method('getBody')
->willReturn('<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"><d:sync-token>' . $syncToken . '</d:sync-token></d:multistatus>');
$client->expects(self::once())
->method('request')
->with('REPORT', 'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2', self::anything())
->willReturn($response);
$this->clientService->method('newClient')
->willReturn($client);
$this->config->method('getSystemValueInt')
->willReturn(30);
$this->config->method('getSystemValue')
->willReturn(false);
$this->federatedCalendarMapper->expects(self::never())
->method('updateSyncTokenAndTime');
@@ -0,0 +1,404 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Federation\FederatedCalendar;
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
use OCA\DAV\CalDAV\Federation\FederatedCalendarObject;
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
use OCP\Constants;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\PropPatch;
use Test\TestCase;
class FederatedCalendarTest extends TestCase {
private FederatedCalendar $federatedCalendar;
private FederatedCalendarMapper&MockObject $federatedCalendarMapper;
private FederatedCalendarSyncService&MockObject $federatedCalendarService;
private CalDavBackend&MockObject $caldavBackend;
private FederatedCalendarEntity $federationInfo;
protected function setUp(): void {
parent::setUp();
$this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class);
$this->federatedCalendarService = $this->createMock(FederatedCalendarSyncService::class);
$this->caldavBackend = $this->createMock(CalDavBackend::class);
$this->federationInfo = new FederatedCalendarEntity();
$this->federationInfo->setId(10);
$this->federationInfo->setPrincipaluri('principals/users/user1');
$this->federationInfo->setUri('calendar-uri');
$this->federationInfo->setDisplayName('Federated Calendar');
$this->federationInfo->setColor('#ff0000');
$this->federationInfo->setSharedBy('user2@nextcloud.remote');
$this->federationInfo->setSharedByDisplayName('User 2');
$this->federationInfo->setPermissions(Constants::PERMISSION_READ);
$this->federationInfo->setLastSync(1234567890);
$this->federatedCalendarMapper->method('findByUri')
->with('principals/users/user1', 'calendar-uri')
->willReturn($this->federationInfo);
$calendarInfo = [
'principaluri' => 'principals/users/user1',
'id' => 10,
'uri' => 'calendar-uri',
];
$this->federatedCalendar = new FederatedCalendar(
$this->federatedCalendarMapper,
$this->federatedCalendarService,
$this->caldavBackend,
$calendarInfo,
);
}
public function testGetResourceId(): void {
$this->assertEquals(10, $this->federatedCalendar->getResourceId());
}
public function testGetName(): void {
$this->assertEquals('calendar-uri', $this->federatedCalendar->getName());
}
public function testSetName(): void {
$this->expectException(MethodNotAllowed::class);
$this->expectExceptionMessage('Renaming federated calendars is not allowed');
$this->federatedCalendar->setName('new-name');
}
public function testGetPrincipalURI(): void {
$this->assertEquals('principals/users/user1', $this->federatedCalendar->getPrincipalURI());
}
public function testGetOwner(): void {
$expected = 'principals/remote-users/' . base64_encode('user2@nextcloud.remote');
$this->assertEquals($expected, $this->federatedCalendar->getOwner());
}
public function testGetGroup(): void {
$this->assertNull($this->federatedCalendar->getGroup());
}
public function testGetACLWithReadOnlyPermissions(): void {
$this->federationInfo->setPermissions(Constants::PERMISSION_READ);
$acl = $this->federatedCalendar->getACL();
$this->assertCount(3, $acl);
// Check basic read permissions
$this->assertEquals('{DAV:}read', $acl[0]['privilege']);
$this->assertTrue($acl[0]['protected']);
$this->assertEquals('{DAV:}read-acl', $acl[1]['privilege']);
$this->assertTrue($acl[1]['protected']);
$this->assertEquals('{DAV:}write-properties', $acl[2]['privilege']);
$this->assertTrue($acl[2]['protected']);
}
public function testGetACLWithCreatePermission(): void {
$this->federationInfo->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE);
$acl = $this->federatedCalendar->getACL();
$this->assertCount(4, $acl);
// Check that create permission is added
$privileges = array_column($acl, 'privilege');
$this->assertContains('{DAV:}bind', $privileges);
}
public function testGetACLWithUpdatePermission(): void {
$this->federationInfo->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE);
$acl = $this->federatedCalendar->getACL();
$this->assertCount(4, $acl);
// Check that update permission is added (write-content, not write-properties which is already in base ACL)
$privileges = array_column($acl, 'privilege');
$this->assertContains('{DAV:}write-content', $privileges);
}
public function testGetACLWithDeletePermission(): void {
$this->federationInfo->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_DELETE);
$acl = $this->federatedCalendar->getACL();
$this->assertCount(4, $acl);
// Check that delete permission is added
$privileges = array_column($acl, 'privilege');
$this->assertContains('{DAV:}unbind', $privileges);
}
public function testGetACLWithAllPermissions(): void {
$this->federationInfo->setPermissions(
Constants::PERMISSION_READ
| Constants::PERMISSION_CREATE
| Constants::PERMISSION_UPDATE
| Constants::PERMISSION_DELETE
);
$acl = $this->federatedCalendar->getACL();
$this->assertCount(6, $acl);
$privileges = array_column($acl, 'privilege');
$this->assertContains('{DAV:}read', $privileges);
$this->assertContains('{DAV:}bind', $privileges);
$this->assertContains('{DAV:}write-content', $privileges);
$this->assertContains('{DAV:}write-properties', $privileges);
$this->assertContains('{DAV:}unbind', $privileges);
}
public function testSetACL(): void {
$this->expectException(MethodNotAllowed::class);
$this->expectExceptionMessage('Changing ACLs on federated calendars is not allowed');
$this->federatedCalendar->setACL([]);
}
public function testGetSupportedPrivilegeSet(): void {
$this->assertNull($this->federatedCalendar->getSupportedPrivilegeSet());
}
public function testGetProperties(): void {
$properties = $this->federatedCalendar->getProperties([
'{DAV:}displayname',
'{http://apple.com/ns/ical/}calendar-color',
]);
$this->assertEquals('Federated Calendar', $properties['{DAV:}displayname']);
$this->assertEquals('#ff0000', $properties['{http://apple.com/ns/ical/}calendar-color']);
}
public function testPropPatchWithDisplayName(): void {
$propPatch = $this->createMock(PropPatch::class);
$propPatch->method('getMutations')
->willReturn([
'{DAV:}displayname' => 'New Calendar Name',
]);
$this->federatedCalendarMapper->expects(self::once())
->method('update')
->willReturnCallback(function (FederatedCalendarEntity $entity) {
$this->assertEquals('New Calendar Name', $entity->getDisplayName());
return $entity;
});
$propPatch->expects(self::once())
->method('setResultCode')
->with('{DAV:}displayname', 200);
$this->federatedCalendar->propPatch($propPatch);
}
public function testPropPatchWithColor(): void {
$propPatch = $this->createMock(PropPatch::class);
$propPatch->method('getMutations')
->willReturn([
'{http://apple.com/ns/ical/}calendar-color' => '#00ff00',
]);
$this->federatedCalendarMapper->expects(self::once())
->method('update')
->willReturnCallback(function (FederatedCalendarEntity $entity) {
$this->assertEquals('#00ff00', $entity->getColor());
return $entity;
});
$propPatch->expects(self::once())
->method('setResultCode')
->with('{http://apple.com/ns/ical/}calendar-color', 200);
$this->federatedCalendar->propPatch($propPatch);
}
public function testPropPatchWithNoMutations(): void {
$propPatch = $this->createMock(PropPatch::class);
$propPatch->method('getMutations')
->willReturn([]);
$this->federatedCalendarMapper->expects(self::never())
->method('update');
$propPatch->expects(self::never())
->method('handle');
$this->federatedCalendar->propPatch($propPatch);
}
public function testGetChildACL(): void {
$this->assertEquals($this->federatedCalendar->getACL(), $this->federatedCalendar->getChildACL());
}
public function testGetLastModified(): void {
$this->assertEquals(1234567890, $this->federatedCalendar->getLastModified());
}
public function testDelete(): void {
$this->federatedCalendarMapper->expects(self::once())
->method('deleteById')
->with(10);
$this->federatedCalendar->delete();
}
public function testCreateDirectory(): void {
$this->expectException(MethodNotAllowed::class);
$this->expectExceptionMessage('Creating nested collection is not allowed');
$this->federatedCalendar->createDirectory('test');
}
public function testCalendarQuery(): void {
$filters = ['comp-filter' => ['name' => 'VEVENT']];
$expectedUris = ['event1.ics', 'event2.ics'];
$this->caldavBackend->expects(self::once())
->method('calendarQuery')
->with(10, $filters, 2) // 2 is CALENDAR_TYPE_FEDERATED
->willReturn($expectedUris);
$result = $this->federatedCalendar->calendarQuery($filters);
$this->assertEquals($expectedUris, $result);
}
public function testGetChild(): void {
$objectData = [
'id' => 1,
'uri' => 'event1.ics',
'calendardata' => 'BEGIN:VCALENDAR...',
];
$this->caldavBackend->expects(self::once())
->method('getCalendarObject')
->with(10, 'event1.ics', 2) // 2 is CALENDAR_TYPE_FEDERATED
->willReturn($objectData);
$child = $this->federatedCalendar->getChild('event1.ics');
$this->assertInstanceOf(FederatedCalendarObject::class, $child);
}
public function testGetChildNotFound(): void {
$this->caldavBackend->expects(self::once())
->method('getCalendarObject')
->with(10, 'nonexistent.ics', 2)
->willReturn(null);
$this->expectException(NotFound::class);
$this->federatedCalendar->getChild('nonexistent.ics');
}
public function testGetChildren(): void {
$objects = [
['id' => 1, 'uri' => 'event1.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
['id' => 2, 'uri' => 'event2.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
];
$this->caldavBackend->expects(self::once())
->method('getCalendarObjects')
->with(10, 2) // 2 is CALENDAR_TYPE_FEDERATED
->willReturn($objects);
$children = $this->federatedCalendar->getChildren();
$this->assertCount(2, $children);
$this->assertInstanceOf(FederatedCalendarObject::class, $children[0]);
$this->assertInstanceOf(FederatedCalendarObject::class, $children[1]);
}
public function testGetMultipleChildren(): void {
$paths = ['event1.ics', 'event2.ics'];
$objects = [
['id' => 1, 'uri' => 'event1.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
['id' => 2, 'uri' => 'event2.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
];
$this->caldavBackend->expects(self::once())
->method('getMultipleCalendarObjects')
->with(10, $paths, 2) // 2 is CALENDAR_TYPE_FEDERATED
->willReturn($objects);
$children = $this->federatedCalendar->getMultipleChildren($paths);
$this->assertCount(2, $children);
$this->assertInstanceOf(FederatedCalendarObject::class, $children[0]);
$this->assertInstanceOf(FederatedCalendarObject::class, $children[1]);
}
public function testChildExists(): void {
$this->caldavBackend->expects(self::once())
->method('getCalendarObject')
->with(10, 'event1.ics', 2)
->willReturn(['id' => 1, 'uri' => 'event1.ics']);
$result = $this->federatedCalendar->childExists('event1.ics');
$this->assertTrue($result);
}
public function testChildNotExists(): void {
$this->caldavBackend->expects(self::once())
->method('getCalendarObject')
->with(10, 'nonexistent.ics', 2)
->willReturn(null);
$result = $this->federatedCalendar->childExists('nonexistent.ics');
$this->assertFalse($result);
}
public function testCreateFile(): void {
$calendarData = 'BEGIN:VCALENDAR...END:VCALENDAR';
$remoteEtag = '"remote-etag-123"';
$localEtag = '"local-etag-456"';
$this->federatedCalendarService->expects(self::once())
->method('createCalendarObject')
->with($this->federationInfo, 'event1.ics', $calendarData)
->willReturn($remoteEtag);
$this->caldavBackend->expects(self::once())
->method('createCalendarObject')
->with(10, 'event1.ics', $calendarData, 2)
->willReturn($localEtag);
$result = $this->federatedCalendar->createFile('event1.ics', $calendarData);
$this->assertEquals($localEtag, $result);
}
public function testUpdateFile(): void {
$calendarData = 'BEGIN:VCALENDAR...UPDATED...END:VCALENDAR';
$remoteEtag = '"remote-etag-updated"';
$localEtag = '"local-etag-updated"';
$this->federatedCalendarService->expects(self::once())
->method('updateCalendarObject')
->with($this->federationInfo, 'event1.ics', $calendarData)
->willReturn($remoteEtag);
$this->caldavBackend->expects(self::once())
->method('updateCalendarObject')
->with(10, 'event1.ics', $calendarData, 2)
->willReturn($localEtag);
$result = $this->federatedCalendar->updateFile('event1.ics', $calendarData);
$this->assertEquals($localEtag, $result);
}
public function testDeleteFile(): void {
$this->federatedCalendarService->expects(self::once())
->method('deleteCalendarObject')
->with($this->federationInfo, 'event1.ics');
$this->caldavBackend->expects(self::once())
->method('deleteCalendarObject')
->with(10, 'event1.ics', 2);
$this->federatedCalendar->deleteFile('event1.ics');
}
}
@@ -17,6 +17,7 @@ use OCP\Config\IUserConfig;
use OCP\IAppConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Security\ISecureRandom;
@@ -163,7 +164,7 @@ class IMipServiceTest extends TestCase {
public function testIsSystemUserWhenUserExists(): void {
$email = 'user@example.com';
$user = $this->createMock(\OCP\IUser::class);
$user = $this->createMock(IUser::class);
$this->userManager->expects(self::once())
->method('getByEmail')
+6
View File
@@ -31,6 +31,8 @@ class CapabilitiesTest extends TestCase {
'dav' => [
'chunking' => '1.0',
'public_shares_chunking' => true,
'search_supports_creation_time' => true,
'search_supports_upload_time' => true,
],
];
$this->assertSame($expected, $capabilities->getCapabilities());
@@ -51,6 +53,8 @@ class CapabilitiesTest extends TestCase {
'dav' => [
'chunking' => '1.0',
'public_shares_chunking' => true,
'search_supports_creation_time' => true,
'search_supports_upload_time' => true,
'bulkupload' => '1.0',
],
];
@@ -72,6 +76,8 @@ class CapabilitiesTest extends TestCase {
'dav' => [
'chunking' => '1.0',
'public_shares_chunking' => true,
'search_supports_creation_time' => true,
'search_supports_upload_time' => true,
'absence-supported' => true,
'absence-replacement' => true,
],
@@ -436,7 +436,7 @@ class EventsSearchProviderTest extends TestCase {
}
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'generateSublineDataProvider')]
public function testGenerateSubline(string $ics, string $expectedSubline): void {
public function testGenerateSubline(string $ics, string $expectedSubline, array $calendarInfo = []): void {
$vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
$eventComponent = $vCalendar->VEVENT;
@@ -449,19 +449,23 @@ class EventsSearchProviderTest extends TestCase {
return $date->format('m-d');
});
$actual = self::invokePrivate($this->provider, 'generateSubline', [$eventComponent]);
$actual = self::invokePrivate($this->provider, 'generateSubline', [$eventComponent, $calendarInfo]);
$this->assertEquals($expectedSubline, $actual);
}
public static function generateSublineDataProvider(): array {
return [
[self::$vEvent1, '08-16 09:00 - 10:00'],
[self::$vEvent2, '08-16 09:00 - 08-17 10:00'],
[self::$vEvent3, '10-05'],
[self::$vEvent4, '10-05 - 10-07'],
[self::$vEvent5, '10-05 - 10-09'],
[self::$vEvent6, '10-05'],
[self::$vEvent7, '08-16 09:00 - 09:00'],
[self::$vEvent1, '08-16 09:00 - 10:00', []],
[self::$vEvent2, '08-16 09:00 - 08-17 10:00', []],
[self::$vEvent3, '10-05', []],
[self::$vEvent4, '10-05 - 10-07', []],
[self::$vEvent5, '10-05 - 10-09', []],
[self::$vEvent6, '10-05', []],
[self::$vEvent7, '08-16 09:00 - 09:00', []],
[self::$vEvent1, '08-16 09:00 - 10:00 (My Calendar)', ['{DAV:}displayname' => 'My Calendar']],
[self::$vEvent3, '10-05 (My Calendar)', ['{DAV:}displayname' => 'My Calendar']],
[self::$vEvent2, '08-16 09:00 - 08-17 10:00 (My Calendar)', ['{DAV:}displayname' => 'My Calendar']],
[self::$vEvent1, '08-16 09:00 - 10:00', ['{DAV:}displayname' => '']],
];
}
}
@@ -290,24 +290,29 @@ class TasksSearchProviderTest extends TestCase {
}
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'generateSublineDataProvider')]
public function testGenerateSubline(string $ics, string $expectedSubline): void {
public function testGenerateSubline(string $ics, string $expectedSubline, array $calendarInfo = []): void {
$vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
$taskComponent = $vCalendar->VTODO;
$this->l10n->method('t')->willReturnArgument(0);
$this->l10n->method('l')->willReturnArgument(0);
$actual = self::invokePrivate($this->provider, 'generateSubline', [$taskComponent]);
$actual = self::invokePrivate($this->provider, 'generateSubline', [$taskComponent, $calendarInfo]);
$this->assertEquals($expectedSubline, $actual);
}
public static function generateSublineDataProvider(): array {
return [
[self::$vTodo0, ''],
[self::$vTodo1, 'Completed on %s'],
[self::$vTodo2, 'Completed on %s'],
[self::$vTodo3, 'Due on %s'],
[self::$vTodo4, 'Due on %s by %s'],
[self::$vTodo0, '', []],
[self::$vTodo1, 'Completed on %s', []],
[self::$vTodo2, 'Completed on %s', []],
[self::$vTodo3, 'Due on %s', []],
[self::$vTodo4, 'Due on %s by %s', []],
[self::$vTodo0, '(My Tasks)', ['{DAV:}displayname' => 'My Tasks']],
[self::$vTodo1, 'Completed on %s (My Tasks)', ['{DAV:}displayname' => 'My Tasks']],
[self::$vTodo3, 'Due on %s (My Tasks)', ['{DAV:}displayname' => 'My Tasks']],
[self::$vTodo4, 'Due on %s by %s (My Tasks)', ['{DAV:}displayname' => 'My Tasks']],
[self::$vTodo1, 'Completed on %s', ['{DAV:}displayname' => '']],
];
}
}
@@ -18,6 +18,7 @@ use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\Uid\Uuid;
@@ -31,6 +32,7 @@ class ExampleContactServiceTest extends TestCase {
protected LoggerInterface&MockObject $logger;
protected IAppConfig&MockObject $appConfig;
protected IAppData&MockObject $appData;
protected IL10N&MockObject $l;
protected function setUp(): void {
parent::setUp();
@@ -39,6 +41,7 @@ class ExampleContactServiceTest extends TestCase {
$this->appDataFactory = $this->createMock(IAppDataFactory::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->appConfig = $this->createMock(IAppConfig::class);
$this->l = $this->createMock((IL10N::class));
$this->appData = $this->createMock(IAppData::class);
$this->appDataFactory->method('get')
@@ -50,6 +53,7 @@ class ExampleContactServiceTest extends TestCase {
$this->appConfig,
$this->logger,
$this->cardDav,
$this->l,
);
}
+10
View File
@@ -33,21 +33,31 @@ OC.L10N.register(
"Please login to the web interface, go to the \"Security\" section of your personal settings and update your encryption password by entering this password into the \"Old login password\" field and your current login password." : "Por favor ingrese en la interfaz web, vaya a la sección \"Seguridad\" de sus ajustes personales y actualice su contraseña de cifrado ingresando esta contraseña en el campo \"Contraseña de inicio de sesión antigua\" y su contraseña actual.",
"Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you." : "No se puede descifrar este archivo, probablemente se trate de un archivo compartido. Por favor, pida al propietario del archivo que vuelva a compartirlo con usted.",
"Cannot read this file, probably this is a shared file. Please ask the file owner to reshare the file with you." : "No se puede leer este archivo, probablemente se trate de un archivo compartido. Por favor, pida al propietario del archivo que vuelva a compartirlo con usted.",
"Default Encryption Module" : "Módulo de cifrado predeterminado",
"Default encryption module for Nextcloud Server-side Encryption (SSE)" : "Módulo de cifrado predeterminado para el cifrado del lado del servidor (SSE) de Nextcloud",
"Enabling this option encrypts all files stored on the main storage, otherwise only files on external storage will be encrypted" : "Al activar esta opción se encriptarán todos los archivos almacenados en la memoria principal, de lo contrario, serán cifrados sólo los archivos de almacenamiento externo",
"Encrypt the home storage" : "Encriptar el almacenamiento personal",
"Disable recovery key" : "Desactiva la clave de recuperación",
"Enable recovery key" : "Activa la clave de recuperación",
"The recovery key is an additional encryption key used to encrypt files. It is used to recover files from an account if the password is forgotten." : "La llave de recuperación es una llave de cifrado adicional utilizada para cifrar archivos. Es utilizada para recuperar los archivos de una cuenta si la contraseña fuese olvidada.",
"Recovery key password" : "Contraseña de clave de recuperación",
"Passwords fields do not match" : "Las contraseñas no coinciden",
"Repeat recovery key password" : "Repita la contraseña de recuperación",
"An error occurred while updating the recovery key settings. Please try again." : "Se produjo un error al actualizar la configuración de la clave de recuperación. Por favor, inténtelo de nuevo.",
"Change recovery key password" : "Cambiar la contraseña de la clave de recuperación",
"Old recovery key password" : "Antigua contraseña de recuperación",
"New recovery key password" : "Nueva contraseña de recuperación",
"Repeat new recovery key password" : "Repita la nueva contraseña de recuperación",
"An error occurred while changing the recovery key password. Please try again." : "Se produjo un error al cambiar la contraseña de la clave de recuperación. Por favor, inténtelo de nuevo.",
"Update private key password" : "Actualizar la contraseña de la clave privada",
"Your private key password no longer matches your log-in password. Set your old private key password to your current log-in password." : "Tu contraseña de clave privada ya no coincide con tu contraseña de inicio de sesión. Cambia tu contraseña de clave privada anterior a tu contraseña de inicio de sesión actual.",
"If you do not remember your old password you can ask your administrator to recover your files." : "Si no recuerda su antigua contraseña, puede pedir a su administrador que recupere sus archivos.",
"Old log-in password" : "Contraseña de acceso antigua",
"Current log-in password" : "Contraseña de acceso actual",
"Update" : "Actualizar",
"Updating recovery keys. This can take some time…" : "Actualizando las claves de recuperación. Esto puede tardar algún tiempo …",
"Enabling this option will allow you to reobtain access to your encrypted files in case of password loss" : "Habilitar esta opción le permitirá volver a tener acceso a sus archivos cifrados en caso de pérdida de contraseña",
"Enable password recovery" : "Habilitar la contraseña de recuperación",
"Default encryption module" : "Módulo de cifrado por defecto",
"Encryption app is enabled but your keys are not initialized, please log-out and log-in again" : "La app de cifrado está habilitada pero sus claves no se han inicializado, por favor, cierre la sesión y vuelva a iniciarla de nuevo.",
"Basic encryption module" : "Módulo de cifrado básico",
+10
View File
@@ -31,21 +31,31 @@
"Please login to the web interface, go to the \"Security\" section of your personal settings and update your encryption password by entering this password into the \"Old login password\" field and your current login password." : "Por favor ingrese en la interfaz web, vaya a la sección \"Seguridad\" de sus ajustes personales y actualice su contraseña de cifrado ingresando esta contraseña en el campo \"Contraseña de inicio de sesión antigua\" y su contraseña actual.",
"Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you." : "No se puede descifrar este archivo, probablemente se trate de un archivo compartido. Por favor, pida al propietario del archivo que vuelva a compartirlo con usted.",
"Cannot read this file, probably this is a shared file. Please ask the file owner to reshare the file with you." : "No se puede leer este archivo, probablemente se trate de un archivo compartido. Por favor, pida al propietario del archivo que vuelva a compartirlo con usted.",
"Default Encryption Module" : "Módulo de cifrado predeterminado",
"Default encryption module for Nextcloud Server-side Encryption (SSE)" : "Módulo de cifrado predeterminado para el cifrado del lado del servidor (SSE) de Nextcloud",
"Enabling this option encrypts all files stored on the main storage, otherwise only files on external storage will be encrypted" : "Al activar esta opción se encriptarán todos los archivos almacenados en la memoria principal, de lo contrario, serán cifrados sólo los archivos de almacenamiento externo",
"Encrypt the home storage" : "Encriptar el almacenamiento personal",
"Disable recovery key" : "Desactiva la clave de recuperación",
"Enable recovery key" : "Activa la clave de recuperación",
"The recovery key is an additional encryption key used to encrypt files. It is used to recover files from an account if the password is forgotten." : "La llave de recuperación es una llave de cifrado adicional utilizada para cifrar archivos. Es utilizada para recuperar los archivos de una cuenta si la contraseña fuese olvidada.",
"Recovery key password" : "Contraseña de clave de recuperación",
"Passwords fields do not match" : "Las contraseñas no coinciden",
"Repeat recovery key password" : "Repita la contraseña de recuperación",
"An error occurred while updating the recovery key settings. Please try again." : "Se produjo un error al actualizar la configuración de la clave de recuperación. Por favor, inténtelo de nuevo.",
"Change recovery key password" : "Cambiar la contraseña de la clave de recuperación",
"Old recovery key password" : "Antigua contraseña de recuperación",
"New recovery key password" : "Nueva contraseña de recuperación",
"Repeat new recovery key password" : "Repita la nueva contraseña de recuperación",
"An error occurred while changing the recovery key password. Please try again." : "Se produjo un error al cambiar la contraseña de la clave de recuperación. Por favor, inténtelo de nuevo.",
"Update private key password" : "Actualizar la contraseña de la clave privada",
"Your private key password no longer matches your log-in password. Set your old private key password to your current log-in password." : "Tu contraseña de clave privada ya no coincide con tu contraseña de inicio de sesión. Cambia tu contraseña de clave privada anterior a tu contraseña de inicio de sesión actual.",
"If you do not remember your old password you can ask your administrator to recover your files." : "Si no recuerda su antigua contraseña, puede pedir a su administrador que recupere sus archivos.",
"Old log-in password" : "Contraseña de acceso antigua",
"Current log-in password" : "Contraseña de acceso actual",
"Update" : "Actualizar",
"Updating recovery keys. This can take some time…" : "Actualizando las claves de recuperación. Esto puede tardar algún tiempo …",
"Enabling this option will allow you to reobtain access to your encrypted files in case of password loss" : "Habilitar esta opción le permitirá volver a tener acceso a sus archivos cifrados en caso de pérdida de contraseña",
"Enable password recovery" : "Habilitar la contraseña de recuperación",
"Default encryption module" : "Módulo de cifrado por defecto",
"Encryption app is enabled but your keys are not initialized, please log-out and log-in again" : "La app de cifrado está habilitada pero sus claves no se han inicializado, por favor, cierre la sesión y vuelva a iniciarla de nuevo.",
"Basic encryption module" : "Módulo de cifrado básico",
+1 -1
View File
@@ -545,7 +545,7 @@ class Crypt {
$options,
$iv);
if ($plainContent) {
if ($plainContent !== false) {
return $plainContent;
} else {
throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
+4
View File
@@ -22,16 +22,20 @@ OC.L10N.register(
"Sharing" : "Delen",
"Federated file sharing" : "Gefedereerd delen",
"Provide federated file sharing across servers" : "Voorzien in gefedereerd delen van bestanden over verschillende servers",
"Enable data upload" : "Schakel data upload in",
"Disable upload" : "Schakel upload uit",
"This is used to retrieve the federated cloud ID to make federated sharing easier." : "Dit wordt gebruikt om de federatieve cloud-ID op te halen om federatief delen gemakkelijker te maken.",
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Deel met mij via mijn #Nextcloud Federated Cloud-ID, zie {url}",
"Share with me through my #Nextcloud Federated Cloud ID" : "Deel met mij via mijn #Nextcloud gefedereerde Cloud-ID",
"Share with me via Nextcloud" : "Deel met mij via Nextcloud",
"Cloud ID copied" : "Cloud ID gekopieerd",
"Copy" : "Kopiëren",
"Clipboard not available. Please copy the cloud ID manually." : "Klembord niet beschikbaar. Kopieer de cloud-ID handmatig.",
"Copied!" : "Gekopieerd!",
"Federated Cloud" : "Gefedereerde Cloud",
"Your Federated Cloud ID" : "Jouw Federated Cloud-ID",
"Share it so your friends can share files with you:" : "Deel het, zodat anderen bestanden met jou kunnen delen:",
"Bluesky" : "Bluesky",
"Facebook" : "Facebook",
"Mastodon" : "Mastodon",
"Add to your website" : "Toevoegen aan je website",
+4
View File
@@ -20,16 +20,20 @@
"Sharing" : "Delen",
"Federated file sharing" : "Gefedereerd delen",
"Provide federated file sharing across servers" : "Voorzien in gefedereerd delen van bestanden over verschillende servers",
"Enable data upload" : "Schakel data upload in",
"Disable upload" : "Schakel upload uit",
"This is used to retrieve the federated cloud ID to make federated sharing easier." : "Dit wordt gebruikt om de federatieve cloud-ID op te halen om federatief delen gemakkelijker te maken.",
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Deel met mij via mijn #Nextcloud Federated Cloud-ID, zie {url}",
"Share with me through my #Nextcloud Federated Cloud ID" : "Deel met mij via mijn #Nextcloud gefedereerde Cloud-ID",
"Share with me via Nextcloud" : "Deel met mij via Nextcloud",
"Cloud ID copied" : "Cloud ID gekopieerd",
"Copy" : "Kopiëren",
"Clipboard not available. Please copy the cloud ID manually." : "Klembord niet beschikbaar. Kopieer de cloud-ID handmatig.",
"Copied!" : "Gekopieerd!",
"Federated Cloud" : "Gefedereerde Cloud",
"Your Federated Cloud ID" : "Jouw Federated Cloud-ID",
"Share it so your friends can share files with you:" : "Deel het, zodat anderen bestanden met jou kunnen delen:",
"Bluesky" : "Bluesky",
"Facebook" : "Facebook",
"Mastodon" : "Mastodon",
"Add to your website" : "Toevoegen aan je website",
@@ -17,6 +17,7 @@ use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Server;
class Application extends App implements IBootstrap {
@@ -41,7 +42,7 @@ class Application extends App implements IBootstrap {
$manager->addCloudFederationProvider($type,
'Federated Files Sharing',
function (): CloudFederationProviderFiles {
return \OCP\Server::get(CloudFederationProviderFiles::class);
return Server::get(CloudFederationProviderFiles::class);
});
}
}
@@ -329,14 +329,29 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATETIME_MUTABLE))
->executeStatement();
// send the updated permission to the owner/initiator, if they are not the same
if ($share->getShareOwner() !== $share->getSharedBy()) {
/*
* If the share owner and share initiator are on the same instance,
* then we're done here as the share was just updated above.
*
* However, if the share owner/sharee is on a remote instance (and thus we're dealing with a federated share),
* then we are supposed to let the share owner/ sharee on the remote instance know.
*/
if ($this->shouldNotifyRemote($share)) {
$this->sendPermissionUpdate($share);
}
return $share;
}
/**
* Notify owner/sharee if they are not the same and ANY of them is a remote user.
*/
protected function shouldNotifyRemote(IShare $share): bool {
$ownerOrSharerIsRemoteUser = !$this->userManager->userExists($share->getShareOwner())
|| !$this->userManager->userExists($share->getSharedBy());
return $ownerOrSharerIsRemoteUser && $share->getShareOwner() !== $share->getSharedBy();
}
/**
* Send the updated permission to the owner/initiator, if they are not the same.
*
@@ -466,13 +481,8 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
* @throws HintException
*/
protected function revokeShare($share, $isOwner) {
if ($this->userManager->userExists($share->getShareOwner()) && $this->userManager->userExists($share->getSharedBy())) {
// If both the owner and the initiator of the share are local users we don't have to notify anybody else
return;
}
// also send a unShare request to the initiator, if this is a different user than the owner
if ($share->getShareOwner() !== $share->getSharedBy()) {
if ($this->shouldNotifyRemote($share)) {
if ($isOwner) {
[, $remote] = $this->addressHandler->splitUserRemote($share->getSharedBy());
} else {
@@ -14,6 +14,7 @@ use OCP\GlobalScale\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IDelegatedSettings;
use OCP\Util;
class Admin implements IDelegatedSettings {
/**
@@ -44,8 +45,8 @@ class Admin implements IDelegatedSettings {
$this->initialState->provideInitialState('lookupServerUploadEnabled', $this->fedShareProvider->isLookupServerUploadEnabled());
$this->initialState->provideInitialState('federatedTrustedShareAutoAccept', $this->fedShareProvider->isFederatedTrustedShareAutoAccept());
\OCP\Util::addStyle(Application::APP_ID, 'settings-admin');
\OCP\Util::addScript(Application::APP_ID, 'settings-admin');
Util::addStyle(Application::APP_ID, 'settings-admin');
Util::addScript(Application::APP_ID, 'settings-admin');
return new TemplateResponse(Application::APP_ID, 'settings-admin', renderAs: '');
}
@@ -16,6 +16,7 @@ use OCP\Defaults;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Settings\ISettings;
use OCP\Util;
class Personal implements ISettings {
public function __construct(
@@ -42,8 +43,8 @@ class Personal implements ISettings {
$this->initialState->provideInitialState('cloudId', $cloudID);
$this->initialState->provideInitialState('docUrlFederated', $this->urlGenerator->linkToDocs('user-sharing-federated'));
\OCP\Util::addStyle(Application::APP_ID, 'settings-personal');
\OCP\Util::addScript(Application::APP_ID, 'settings-personal');
Util::addStyle(Application::APP_ID, 'settings-personal');
Util::addScript(Application::APP_ID, 'settings-personal');
return new TemplateResponse(Application::APP_ID, 'settings-personal', renderAs: TemplateResponse::RENDER_AS_BLANK);
}
@@ -0,0 +1,417 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\FederatedFileSharing\Tests;
use LogicException;
use OC\Federation\CloudId;
use OC\Share20\Share;
use OCA\FederatedFileSharing\AddressHandler;
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\FederatedFileSharing\Notifications;
use OCA\FederatedFileSharing\TokenHandler;
use OCP\Constants;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IExpressionBuilder;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudIdManager;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\GlobalScale\IConfig as GlobalScaleConfig;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IUserManager;
use OCP\Share\IShare;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
class FederatedShareProviderReshareRemoteTest extends \Test\TestCase {
private IDBConnection&MockObject $connection;
private AddressHandler&MockObject $addressHandler;
private Notifications&MockObject $notifications;
private TokenHandler&MockObject $tokenHandler;
private IL10N&MockObject $l10n;
private IRootFolder&MockObject $rootFolder;
private IConfig&MockObject $config;
private IUserManager&MockObject $userManager;
private ICloudIdManager&MockObject $cloudIdManager;
private GlobalScaleConfig&MockObject $gsConfig;
private ICloudFederationProviderManager&MockObject $cloudFederationProviderManager;
private LoggerInterface $logger;
private FederatedShareProvider $shareProvider;
protected function setUp(): void {
$this->connection = $this->createMock(IDBConnection::class);
$this->addressHandler = $this->createMock(AddressHandler::class);
$this->notifications = $this->createMock(Notifications::class);
$this->tokenHandler = $this->createMock(TokenHandler::class);
$this->l10n = $this->createMock(IL10N::class);
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->config = $this->createMock(IConfig::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
$this->gsConfig = $this->createMock(GlobalScaleConfig::class);
$this->cloudFederationProviderManager = $this->createMock(ICloudFederationProviderManager::class);
$this->logger = new NullLogger();
$this->shareProvider = new FederatedShareProvider(
$this->connection,
$this->addressHandler,
$this->notifications,
$this->tokenHandler,
$this->l10n,
$this->rootFolder,
$this->config,
$this->userManager,
$this->cloudIdManager,
$this->gsConfig,
$this->cloudFederationProviderManager,
$this->logger,
);
}
/**
* This test case validates that requestReShare is called when creating a federated share.
*
* We have three actors:
*
* jane@https://origin.test
* alice@https://local.test
* bob@https://destination.test
*
* Jane shared the folder with Alice which re-shares the folder with Bob.
*
* The expected outcome is, that Alice sends a request to Jane to share the folder with Bob.
*/
public function testCreateRemoteOwner(): void {
$permissions = Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE;
$node = $this->createMock(Folder::class);
$node->method('getId')->willReturn(1000);
$node->method('getName')->willReturn('Share 1');
/*
* Mocks getSharedWith ($alreadyShared and $alreadySharedGroup).
* The share we are going to create does not already exist.
*/
$expr1 = $this->createMock(IExpressionBuilder::class);
$expr1->method('in')->willReturn('');
$expr1->method('eq')->willReturn('');
$result1 = $this->createMock(IResult::class);
$result1->method('fetchAssociative')->willReturn(false);
$qb1 = $this->createMock(IQueryBuilder::class);
$qb1->method('select')->willReturnSelf();
$qb1->method('from')->willReturnSelf();
$qb1->method('where')->willReturnSelf();
$qb1->method('expr')->willReturn($expr1);
$qb1->method('createNamedParameter')->willReturn('');
$qb1->method('executeQuery')->willReturn($result1);
/*
* Mocks for getShareFromExternalShareTable.
* The share we are going to create is an external share.
*/
$expr2 = $this->createMock(IExpressionBuilder::class);
$expr2->method('eq')->willReturn('');
$result2 = $this->createMock(IResult::class);
$result2->method('fetchAllAssociative')->willReturn([
[
'id' => 100000,
'parent' => -1,
'share_type' => 0,
'remote' => 'https://origin.test/',
'remote_id' => '10',
'share_token' => 'share_token1',
'password' => '',
'name' => '/Share1',
'owner' => 'jane', // owner in share_external is the user on the remote instance
'user' => 'alice', // user in share_external is the receiver on the current instance
'mountpoint' => '/Share1',
'mountpoint_hash' => '94ee935396a30e27953838d0f65d1e17', // md5(mountpoint)
'accepted' => 1,
],
]);
$qb2 = $this->createMock(IQueryBuilder::class);
$qb2->method('select')->willReturnSelf();
$qb2->method('from')->willReturnSelf();
$qb2->method('where')->willReturnSelf();
$qb2->method('expr')->willReturn($expr2);
$qb2->method('createNamedParameter')->willReturn('');
$qb2->method('executeQuery')->willReturn($result2);
/*
* Mocks for addShareToDB.
* The record on the local instance for the outgoing share.
*/
$expr3 = $this->createMock(IExpressionBuilder::class);
$expr3->method('eq')->willReturn('');
$result3 = $this->createMock(IResult::class);
$result3->method('fetchAllAssociative')->willReturn([
[
'id' => 100000,
'parent' => -1,
'share_type' => 0,
'remote' => 'https://origin.test/',
'remote_id' => '10',
'share_token' => 'share_token2',
'password' => '',
'name' => '/Share1',
'owner' => 'jane', // owner in share_external is the user on the remote instance
'user' => 'alice', // user in share_external is the receiver on the current instance
'mountpoint' => '/Share1',
'mountpoint_hash' => '94ee935396a30e27953838d0f65d1e17', // md5(mountpoint)
'accepted' => 1,
],
]);
$qb3 = $this->createMock(IQueryBuilder::class);
$qb3->method('insert')->willReturnSelf();
$qb3->method('setValue')->willReturnSelf();
$qb3->method('getLastInsertId')->willReturn(2000);
/*
* Mocks for updateSuccessfulReShare
*/
$expr4 = $this->createMock(IExpressionBuilder::class);
$expr4->method('eq')->willReturn('');
$qb4 = $this->createMock(IQueryBuilder::class);
$qb4->method('update')->willReturnSelf();
$qb4->method('where')->willReturnSelf();
$qb4->method('expr')->willReturn($expr4);
$qb4->method('set')->willReturnSelf();
$qb4->method('createNamedParameter')->willReturn('');
/*
* Mocks for storeRemoteId.
*/
$qb5 = $this->createMock(IQueryBuilder::class);
$qb5->method('insert')->willReturnSelf();
$qb5->method('values')->willReturnSelf();
/*
* Mocks for getRawShare.
*/
$expr6 = $this->createMock(IExpressionBuilder::class);
$expr6->method('eq')->willReturn('');
$result6 = $this->createMock(IResult::class);
$result6->method('fetchAssociative')->willReturn([
'id' => 20000,
'share_type' => IShare::TYPE_REMOTE,
'share_with' => 'bob@https://destination.test',
'password' => null,
'uid_owner' => 'jane@origin.test',
'uid_initiator' => 'alice',
'parent' => null,
'item_type' => 'folder',
'item_source' => (string)$node->getId(),
'item_target' => null,
'file_source' => $node->getId(),
'file_target' => '',
'permissions' => $permissions,
'stime' => 0,
'accepted' => 0,
'expiration' => null,
'token' => 'share_token3',
'mail_send' => 0,
'share_name' => null,
'password_by_talk' => 0,
'note' => null,
'hide_download' => 0,
'label' => null,
'attributes' => null,
'password_expiration_time' => null,
'reminder_sent' => 0,
]);
$qb6 = $this->createMock(IQueryBuilder::class);
$qb6->method('select')->willReturnSelf();
$qb6->method('from')->willReturnSelf();
$qb6->method('where')->willReturnSelf();
$qb6->method('expr')->willReturn($expr6);
$qb6->method('createNamedParameter')->willReturn('');
$qb6->method('executeQuery')->willReturn($result6);
$queryBuilderMatcher = $this->exactly(8);
$this->connection
->expects($queryBuilderMatcher)
->method('getQueryBuilder')
->willReturnCallback(function () use ($queryBuilderMatcher, $qb1, $qb2, $qb3, $qb4, $qb5, $qb6) {
return match ($queryBuilderMatcher->numberOfInvocations()) {
1, 2 => $qb1,
3, 5 => $qb2,
4 => $qb3,
6 => $qb4,
7 => $qb5,
8 => $qb6,
default => throw new LogicException('Unexpected number of invocations for getQueryBuilder')
};
});
// the cloud id for the recipient
$this->cloudIdManager->method('resolveCloudId')
->with('bob@https://destination.test')
->willReturn(new CloudId(
'bob@https://destination.test',
'bob',
'https://destination.test',
'Bob', // is usually null in prod, setting it here to avoid additional mocking
));
$this->addressHandler->method('generateRemoteURL')
->willReturn('https://local.test');
$this->addressHandler->method('compareAddresses')
->willReturn(false);
// the cloud id of the actual owner
$this->cloudIdManager->method('getCloudId')
->willReturn(new CloudId(
'jane@https://origin.test',
'jane',
'https://origin.test',
'Jane', // is usually null in prod, setting it here to avoid additional mocking
));
$this->notifications->expects($this->once())
->method('requestReShare')
->with(
$this->equalTo('share_token1'),
$this->equalTo('10'),
$this->equalTo('2000'),
$this->equalTo('https://origin.test/'),
$this->equalTo('bob@https://destination.test'),
$this->equalTo($permissions),
$this->equalTo('Share 1'),
$this->equalTo(IShare::TYPE_REMOTE),
)
->willReturn(['share_token2', '20']);
$share = new Share($this->rootFolder, $this->userManager);
$share
->setSharedWith('bob@https://destination.test')
->setShareOwner('alice')
->setSharedBy('alice')
->setPermissions($permissions)
->setShareType(IShare::TYPE_REMOTE)
->setNode($node)
->setTarget('/Share1');
$this->shareProvider->create($share);
}
/**
* This test case validates that sendPermission is called when updating a federated share.
*
* We have three actors:
*
* jane@https://origin.test
* alice@https://local.test
* bob@https://destination.test
*
* Jane shared the folder with Alice which re-shared the folder with Bob.
* Alice is now changing the permissions for the share.
*
* The expected outcome is, that Alice sends a request to Jane to change the share.
*/
public function testUpdateRemoteOwner(): void {
$permissions = Constants::PERMISSION_READ;
$node = $this->createMock(Folder::class);
$node->method('getId')->willReturn(1000);
$node->method('getName')->willReturn('Share 1');
/*
* Mocks update share.
*/
$expr1 = $this->createMock(IExpressionBuilder::class);
$expr1->method('eq')->willReturn('');
$qb1 = $this->createMock(IQueryBuilder::class);
$qb1->method('update')->willReturnSelf();
$qb1->method('where')->willReturnSelf();
$qb1->method('expr')->willReturn($expr1);
$qb1->method('createNamedParameter')->willReturn('');
$qb1->method('set')->willReturnSelf();
/*
* Mocks getRemoteId.
*/
$expr2 = $this->createMock(IExpressionBuilder::class);
$expr2->method('eq')->willReturn('');
$result2 = $this->createMock(IResult::class);
$result2->method('fetchAssociative')->willReturn([
'share_id' => 3000,
'remote_id' => '10',
]);
$qb2 = $this->createMock(IQueryBuilder::class);
$qb2->method('select')->willReturnSelf();
$qb2->method('from')->willReturnSelf();
$qb2->method('where')->willReturnSelf();
$qb2->method('expr')->willReturn($expr2);
$qb2->method('createNamedParameter')->willReturn('');
$qb2->method('executeQuery')->willReturn($result2);
$queryBuilderMatcher = $this->exactly(2);
$this->connection
->expects($queryBuilderMatcher)
->method('getQueryBuilder')
->willReturnCallback(function () use ($queryBuilderMatcher, $qb1, $qb2) {
return match ($queryBuilderMatcher->numberOfInvocations()) {
1 => $qb1,
2 => $qb2,
default => throw new LogicException('Unexpected number of invocations for getQueryBuilder')
};
});
$this->userManager->method('userExists')
->willReturnMap([
['jane@https://origin.test', false],
['alice', true],
]);
$this->addressHandler->method('splitUserRemote')
->willReturn(['jane', 'https://origin.test']);
$this->notifications->expects($this->once())
->method('sendPermissionChange')
->with(
$this->equalTo('https://origin.test'),
$this->equalTo('10'),
$this->equalTo('share_token3'),
$this->equalTo($permissions),
);
$share = new Share($this->rootFolder, $this->userManager);
$share
->setId('3000')
->setToken('share_token3')
->setSharedWith('bob@https://destination.test')
->setShareOwner('jane@https://origin.test')
->setSharedBy('alice')
->setPermissions($permissions)
->setShareType(IShare::TYPE_REMOTE)
->setNode($node)
->setTarget('/Share1');
$this->shareProvider->update($share);
}
}
@@ -450,11 +450,7 @@ class FederatedShareProviderTest extends \Test\TestCase {
$sharedBy . '@http://localhost'
)->willReturn(true);
if ($owner === $sharedBy) {
$this->provider->expects($this->never())->method('sendPermissionUpdate');
} else {
$this->provider->expects($this->once())->method('sendPermissionUpdate');
}
$this->provider->expects($this->never())->method('sendPermissionUpdate');
$this->rootFolder->expects($this->never())->method($this->anything());
+1 -5
View File
@@ -328,10 +328,6 @@ OC.L10N.register(
"Tags" : "الوسوم",
"Save as …" : "حفظ باسم ...",
"Converting files …" : "تحويل الملفات ...",
"Converting file …" : "تحويل الملف ...",
"Moving \"{source}\" to \"{destination}\" …" : "نقل \"{source}\" إلى \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "نسخ \"{source}\" إلى \"{destination}\" …",
"(copy)" : "(نسخ)",
"(copy %n)" : "(نسخ %n)"
"Converting file …" : "تحويل الملف ..."
},
"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;");
+1 -5
View File
@@ -326,10 +326,6 @@
"Tags" : "الوسوم",
"Save as …" : "حفظ باسم ...",
"Converting files …" : "تحويل الملفات ...",
"Converting file …" : "تحويل الملف ...",
"Moving \"{source}\" to \"{destination}\" …" : "نقل \"{source}\" إلى \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "نسخ \"{source}\" إلى \"{destination}\" …",
"(copy)" : "(نسخ)",
"(copy %n)" : "(نسخ %n)"
"Converting file …" : "تحويل الملف ..."
},"pluralForm" :"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;"
}
+1 -3
View File
@@ -245,8 +245,6 @@ OC.L10N.register(
"You" : "Tu",
"Shared multiple times with different people" : "Compartióse múltiples vegaes con otres persones",
"Error while loading the file data" : "Hebo un error mentanto de cargaben los datos de los ficheros",
"Tags" : "Etiquetes",
"(copy)" : "(copia)",
"(copy %n)" : "(copia %n)"
"Tags" : "Etiquetes"
},
"nplurals=2; plural=(n != 1);");
+1 -3
View File
@@ -243,8 +243,6 @@
"You" : "Tu",
"Shared multiple times with different people" : "Compartióse múltiples vegaes con otres persones",
"Error while loading the file data" : "Hebo un error mentanto de cargaben los datos de los ficheros",
"Tags" : "Etiquetes",
"(copy)" : "(copia)",
"(copy %n)" : "(copia %n)"
"Tags" : "Etiquetes"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
+1 -5
View File
@@ -390,10 +390,6 @@ OC.L10N.register(
"Tags" : "Тэгі",
"Save as …" : "Захаваць як …",
"Converting files …" : "Канвертацыя файлаў …",
"Converting file …" : "Канвертацыя файла …",
"Moving \"{source}\" to \"{destination}\" …" : "Перамяшчэнне \"{source}\" у \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Капіяванне \"{source}\" у \"{destination}\" …",
"(copy)" : "(копія)",
"(copy %n)" : "(копія %n)"
"Converting file …" : "Канвертацыя файла …"
},
"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);");
+1 -5
View File
@@ -388,10 +388,6 @@
"Tags" : "Тэгі",
"Save as …" : "Захаваць як …",
"Converting files …" : "Канвертацыя файлаў …",
"Converting file …" : "Канвертацыя файла …",
"Moving \"{source}\" to \"{destination}\" …" : "Перамяшчэнне \"{source}\" у \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Капіяванне \"{source}\" у \"{destination}\" …",
"(copy)" : "(копія)",
"(copy %n)" : "(копія %n)"
"Converting file …" : "Канвертацыя файла …"
},"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"
}
+1 -5
View File
@@ -397,10 +397,6 @@ OC.L10N.register(
"Tags" : "Етикети",
"Save as …" : "Запази като...",
"Converting files …" : "Конвертиране на файлове...",
"Converting file …" : "Файлът се преобразува...",
"Moving \"{source}\" to \"{destination}\" …" : "Преместване на \"{source}\" към \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Копиране на \"{source}\" в \"{destination}\" …",
"(copy)" : "(копие)",
"(copy %n)" : "(копирай %n)"
"Converting file …" : "Файлът се преобразува..."
},
"nplurals=2; plural=(n != 1);");
+1 -5
View File
@@ -395,10 +395,6 @@
"Tags" : "Етикети",
"Save as …" : "Запази като...",
"Converting files …" : "Конвертиране на файлове...",
"Converting file …" : "Файлът се преобразува...",
"Moving \"{source}\" to \"{destination}\" …" : "Преместване на \"{source}\" към \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Копиране на \"{source}\" в \"{destination}\" …",
"(copy)" : "(копие)",
"(copy %n)" : "(копирай %n)"
"Converting file …" : "Файлът се преобразува..."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
+1 -5
View File
@@ -326,10 +326,6 @@ OC.L10N.register(
"Tags" : "Etiquetes",
"Save as …" : "Anomena i desa …",
"Converting files …" : "Convertint fitxers …",
"Converting file …" : "S'està convertint el fitxer …",
"Moving \"{source}\" to \"{destination}\" …" : "S'està movent \"{source}\" a \"{destination}”…",
"Copying \"{source}\" to \"{destination}\" …" : "S'està copiant \"{source}\" a \"{destination}” …",
"(copy)" : "(còpia)",
"(copy %n)" : "(còpia %n)"
"Converting file …" : "S'està convertint el fitxer …"
},
"nplurals=2; plural=(n != 1);");
+1 -5
View File
@@ -324,10 +324,6 @@
"Tags" : "Etiquetes",
"Save as …" : "Anomena i desa …",
"Converting files …" : "Convertint fitxers …",
"Converting file …" : "S'està convertint el fitxer …",
"Moving \"{source}\" to \"{destination}\" …" : "S'està movent \"{source}\" a \"{destination}”…",
"Copying \"{source}\" to \"{destination}\" …" : "S'està copiant \"{source}\" a \"{destination}” …",
"(copy)" : "(còpia)",
"(copy %n)" : "(còpia %n)"
"Converting file …" : "S'està convertint el fitxer …"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
+1 -5
View File
@@ -411,10 +411,6 @@ OC.L10N.register(
"Tags" : "Štítky",
"Save as …" : "Uložit jako …",
"Converting files …" : "Převádění souborů …",
"Converting file …" : "Převádění souboru …",
"Moving \"{source}\" to \"{destination}\" …" : "Přesouvání „{source}“ do „{destination}“ …",
"Copying \"{source}\" to \"{destination}\" …" : "Kopírování „{source}“ do „{destination}“ …",
"(copy)" : "(zkopírovat)",
"(copy %n)" : "(zkopírovat %n)"
"Converting file …" : "Převádění souboru …"
},
"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;");
+1 -5
View File
@@ -409,10 +409,6 @@
"Tags" : "Štítky",
"Save as …" : "Uložit jako …",
"Converting files …" : "Převádění souborů …",
"Converting file …" : "Převádění souboru …",
"Moving \"{source}\" to \"{destination}\" …" : "Přesouvání „{source}“ do „{destination}“ …",
"Copying \"{source}\" to \"{destination}\" …" : "Kopírování „{source}“ do „{destination}“ …",
"(copy)" : "(zkopírovat)",
"(copy %n)" : "(zkopírovat %n)"
"Converting file …" : "Převádění souboru …"
},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;"
}
+1 -5
View File
@@ -395,10 +395,6 @@ OC.L10N.register(
"Tags" : "Tags",
"Save as …" : "Gem som ...",
"Converting files …" : "Konverterer filer ...",
"Converting file …" : "Konverterer fil ...",
"Moving \"{source}\" to \"{destination}\" …" : "Flytter \"{source}\" til \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Kopierer \"{source}\" til \"{destination}\" …",
"(copy)" : "(kopier)",
"(copy %n)" : "(kopier %n)"
"Converting file …" : "Konverterer fil ..."
},
"nplurals=2; plural=(n != 1);");
+1 -5
View File
@@ -393,10 +393,6 @@
"Tags" : "Tags",
"Save as …" : "Gem som ...",
"Converting files …" : "Konverterer filer ...",
"Converting file …" : "Konverterer fil ...",
"Moving \"{source}\" to \"{destination}\" …" : "Flytter \"{source}\" til \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Kopierer \"{source}\" til \"{destination}\" …",
"(copy)" : "(kopier)",
"(copy %n)" : "(kopier %n)"
"Converting file …" : "Konverterer fil ..."
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
+2 -5
View File
@@ -95,6 +95,7 @@ OC.L10N.register(
"Another entry with the same name already exists." : "Ein anderer Eintrag mit diesem Namen existiert bereits.",
"Invalid filename." : "Ungültiger Dateiname.",
"Rename file" : "Datei umbenennen",
"Recently created" : "Zuletzt erstellt",
"Folder" : "Ordner",
"Unknown file type" : "Unbekannter Dateityp",
"{ext} image" : "{ext}-Bild",
@@ -411,10 +412,6 @@ OC.L10N.register(
"Tags" : "Schlagworte",
"Save as …" : "Speichern als …",
"Converting files …" : "Dateien werden konvertiert …",
"Converting file …" : "Datei wird konvertiert …",
"Moving \"{source}\" to \"{destination}\" …" : "Verschiebe \"{source}\" nach \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Kopiere \"{source}\" nach \"{destination}\" …",
"(copy)" : "(Kopie)",
"(copy %n)" : "(Kopie %n)"
"Converting file …" : "Datei wird konvertiert …"
},
"nplurals=2; plural=(n != 1);");
+2 -5
View File
@@ -93,6 +93,7 @@
"Another entry with the same name already exists." : "Ein anderer Eintrag mit diesem Namen existiert bereits.",
"Invalid filename." : "Ungültiger Dateiname.",
"Rename file" : "Datei umbenennen",
"Recently created" : "Zuletzt erstellt",
"Folder" : "Ordner",
"Unknown file type" : "Unbekannter Dateityp",
"{ext} image" : "{ext}-Bild",
@@ -409,10 +410,6 @@
"Tags" : "Schlagworte",
"Save as …" : "Speichern als …",
"Converting files …" : "Dateien werden konvertiert …",
"Converting file …" : "Datei wird konvertiert …",
"Moving \"{source}\" to \"{destination}\" …" : "Verschiebe \"{source}\" nach \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Kopiere \"{source}\" nach \"{destination}\" …",
"(copy)" : "(Kopie)",
"(copy %n)" : "(Kopie %n)"
"Converting file …" : "Datei wird konvertiert …"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
+2 -5
View File
@@ -95,6 +95,7 @@ OC.L10N.register(
"Another entry with the same name already exists." : "Ein anderer Eintrag mit diesem Namen existiert bereits.",
"Invalid filename." : "Ungültiger Dateiname.",
"Rename file" : "Datei umbenennen",
"Recently created" : "Zuletzt erstellt",
"Folder" : "Ordner",
"Unknown file type" : "Unbekannter Dateityp",
"{ext} image" : "{ext}-Bild",
@@ -411,10 +412,6 @@ OC.L10N.register(
"Tags" : "Schlagworte",
"Save as …" : "Speichern als …",
"Converting files …" : "Dateien werden konvertiert …",
"Converting file …" : "Datei wird konvertiert …",
"Moving \"{source}\" to \"{destination}\" …" : "Verschiebe \"{source}\" nach \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Kopiere \"{source}\" nach \"{destination}\" …",
"(copy)" : "(Kopie)",
"(copy %n)" : "(Kopie %n)"
"Converting file …" : "Datei wird konvertiert …"
},
"nplurals=2; plural=(n != 1);");
+2 -5
View File
@@ -93,6 +93,7 @@
"Another entry with the same name already exists." : "Ein anderer Eintrag mit diesem Namen existiert bereits.",
"Invalid filename." : "Ungültiger Dateiname.",
"Rename file" : "Datei umbenennen",
"Recently created" : "Zuletzt erstellt",
"Folder" : "Ordner",
"Unknown file type" : "Unbekannter Dateityp",
"{ext} image" : "{ext}-Bild",
@@ -409,10 +410,6 @@
"Tags" : "Schlagworte",
"Save as …" : "Speichern als …",
"Converting files …" : "Dateien werden konvertiert …",
"Converting file …" : "Datei wird konvertiert …",
"Moving \"{source}\" to \"{destination}\" …" : "Verschiebe \"{source}\" nach \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Kopiere \"{source}\" nach \"{destination}\" …",
"(copy)" : "(Kopie)",
"(copy %n)" : "(Kopie %n)"
"Converting file …" : "Datei wird konvertiert …"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
+1 -5
View File
@@ -409,10 +409,6 @@ OC.L10N.register(
"Tags" : "Ετικέτες",
"Save as …" : "Αποθήκευση ως …",
"Converting files …" : "Μετατροπή αρχείων …",
"Converting file …" : "Μετατροπή αρχείου …",
"Moving \"{source}\" to \"{destination}\" …" : "Μετακίνηση του \"{source}\" στο \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Αντιγραφή του \"{source}\" στο \"{destination}\" …",
"(copy)" : "(αντιγραφή)",
"(copy %n)" : "(αντιγραφή %n)"
"Converting file …" : "Μετατροπή αρχείου …"
},
"nplurals=2; plural=(n != 1);");
+1 -5
View File
@@ -407,10 +407,6 @@
"Tags" : "Ετικέτες",
"Save as …" : "Αποθήκευση ως …",
"Converting files …" : "Μετατροπή αρχείων …",
"Converting file …" : "Μετατροπή αρχείου …",
"Moving \"{source}\" to \"{destination}\" …" : "Μετακίνηση του \"{source}\" στο \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Αντιγραφή του \"{source}\" στο \"{destination}\" …",
"(copy)" : "(αντιγραφή)",
"(copy %n)" : "(αντιγραφή %n)"
"Converting file …" : "Μετατροπή αρχείου …"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}
+2 -5
View File
@@ -95,6 +95,7 @@ OC.L10N.register(
"Another entry with the same name already exists." : "Another entry with the same name already exists.",
"Invalid filename." : "Invalid filename.",
"Rename file" : "Rename file",
"Recently created" : "Recently created",
"Folder" : "Folder",
"Unknown file type" : "Unknown file type",
"{ext} image" : "{ext} image",
@@ -411,10 +412,6 @@ OC.L10N.register(
"Tags" : "Tags",
"Save as …" : "Save as …",
"Converting files …" : "Converting files …",
"Converting file …" : "Converting file …",
"Moving \"{source}\" to \"{destination}\" …" : "Moving \"{source}\" to \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Copying \"{source}\" to \"{destination}\" …",
"(copy)" : "(copy)",
"(copy %n)" : "(copy %n)"
"Converting file …" : "Converting file …"
},
"nplurals=2; plural=(n != 1);");
+2 -5
View File
@@ -93,6 +93,7 @@
"Another entry with the same name already exists." : "Another entry with the same name already exists.",
"Invalid filename." : "Invalid filename.",
"Rename file" : "Rename file",
"Recently created" : "Recently created",
"Folder" : "Folder",
"Unknown file type" : "Unknown file type",
"{ext} image" : "{ext} image",
@@ -409,10 +410,6 @@
"Tags" : "Tags",
"Save as …" : "Save as …",
"Converting files …" : "Converting files …",
"Converting file …" : "Converting file …",
"Moving \"{source}\" to \"{destination}\" …" : "Moving \"{source}\" to \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Copying \"{source}\" to \"{destination}\" …",
"(copy)" : "(copy)",
"(copy %n)" : "(copy %n)"
"Converting file …" : "Converting file …"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}

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