Compare commits
261 Commits
perf/base/
...
jtr/refact
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1abb9ed38 | ||
|
|
600917905e | ||
|
|
460c1fbf6c | ||
|
|
35a7cfdafe | ||
|
|
c0ed1ab40a | ||
|
|
be8efa719b | ||
|
|
98583d7ceb | ||
|
|
2bb9524c84 | ||
|
|
201a97a4ed | ||
|
|
551973cb3e | ||
|
|
f4feec424c | ||
|
|
56dcfc4b86 | ||
|
|
7a96dbc20c | ||
|
|
8c6d314b0e | ||
|
|
ddf79066b9 | ||
|
|
ed3a230403 | ||
|
|
5dbbe2ccf9 | ||
|
|
045ad43237 | ||
|
|
83956e9f04 | ||
|
|
c8380b1b62 | ||
|
|
5ff62d5baf | ||
|
|
eb5a9085ee | ||
|
|
3904da98e6 | ||
|
|
e07be2b189 | ||
|
|
e3c2920c18 | ||
|
|
9b54383532 | ||
|
|
78cfe3742a | ||
|
|
9b54b06de5 | ||
|
|
509784cff2 | ||
|
|
f0f38fd04c | ||
|
|
9f5c3e39a7 | ||
|
|
a304a54775 | ||
|
|
8895970019 | ||
|
|
0a7420d0a4 | ||
|
|
1a70d192f3 | ||
|
|
14e837ce11 | ||
|
|
af78da59bc | ||
|
|
dc529fa86d | ||
|
|
a119716a7d | ||
|
|
44ec45e941 | ||
|
|
38a42d9ad4 | ||
|
|
aa65c7598e | ||
|
|
cf124e7c75 | ||
|
|
3e78bf662d | ||
|
|
783e2ac625 | ||
|
|
7dccf5f257 | ||
|
|
1425df49d0 | ||
|
|
b0240a43af | ||
|
|
347d786a14 | ||
|
|
8417db5df8 | ||
|
|
810caa3e4a | ||
|
|
c98244a79e | ||
|
|
17ef1dbec9 | ||
|
|
eb5c6acf13 | ||
|
|
140bfa2d1f | ||
|
|
082664c0c2 | ||
|
|
397454ff4a | ||
|
|
842b3f9e0a | ||
|
|
7fa44717e0 | ||
|
|
39c14c383b | ||
|
|
af94028bdb | ||
|
|
6f1fc07868 | ||
|
|
27c438503b | ||
|
|
7ae8aebab3 | ||
|
|
cbe8e4d90f | ||
|
|
8b235c8f49 | ||
|
|
92cf64f798 | ||
|
|
938b9ba0ac | ||
|
|
56eaf1dbcc | ||
|
|
3985555753 | ||
|
|
fe612bb4dd | ||
|
|
e958fa0113 | ||
|
|
2613f3274b | ||
|
|
715d776649 | ||
|
|
15591054ac | ||
|
|
304c9fbc3b | ||
|
|
b878678562 | ||
|
|
521bb9432e | ||
|
|
6fce5aa3ea | ||
|
|
e3c6702cd8 | ||
|
|
9cd3177bb4 | ||
|
|
6a29ea172b | ||
|
|
ed1b0dcd5e | ||
|
|
bd8aff39f2 | ||
|
|
fc354f46bb | ||
|
|
7b305fbe22 | ||
|
|
8aedcb4a82 | ||
|
|
842f3a44cb | ||
|
|
01fa73831f | ||
|
|
b40d230218 | ||
|
|
5d84d79c4b | ||
|
|
26ba3cac9c | ||
|
|
85b0dcca27 | ||
|
|
687d82843f | ||
|
|
4da21d78d0 | ||
|
|
142ddaf146 | ||
|
|
f3ceded754 | ||
|
|
9884dec646 | ||
|
|
d14c4082e1 | ||
|
|
052d62b83c | ||
|
|
ceb1b7e767 | ||
|
|
2faa5e894b | ||
|
|
277a35ee23 | ||
|
|
44e733bae9 | ||
|
|
c29c7023d9 | ||
|
|
f43a4c62d6 | ||
|
|
2a2f42e781 | ||
|
|
35ef9b078a | ||
|
|
c63eb85966 | ||
|
|
5c19d5eb26 | ||
|
|
da7250f05d | ||
|
|
9afbad3865 | ||
|
|
80184b4d25 | ||
|
|
074df09caf | ||
|
|
7dba462eec | ||
|
|
0e0af702ac | ||
|
|
f6c79c0d33 | ||
|
|
2909821cb2 | ||
|
|
3542dbf9a1 | ||
|
|
cae14d356b | ||
|
|
3836eb6b0d | ||
|
|
3458d3150b | ||
|
|
03f93d97e9 | ||
|
|
3a21ad408b | ||
|
|
7f89490cef | ||
|
|
34484b591a | ||
|
|
fa2660a9d7 | ||
|
|
4d58563991 | ||
|
|
d039cb92f9 | ||
|
|
128137dc06 | ||
|
|
882d8324e8 | ||
|
|
5fa368c41c | ||
|
|
0e6c8ec73f | ||
|
|
279d5c226e | ||
|
|
52e8de9482 | ||
|
|
95e3dba34c | ||
|
|
3b53e9aeca | ||
|
|
987a52b87f | ||
|
|
1bc0bd7611 | ||
|
|
2d39c5a179 | ||
|
|
6c1d648f59 | ||
|
|
d5d34adcbb | ||
|
|
ace9eb21e6 | ||
|
|
9ce1ae8e61 | ||
|
|
958e67687b | ||
|
|
e7c4dbf2cb | ||
|
|
7f18934344 | ||
|
|
8431abff32 | ||
|
|
e0bf0737a0 | ||
|
|
10ae88a1c3 | ||
|
|
b729dc4ead | ||
|
|
2b5a26db95 | ||
|
|
4588c7134d | ||
|
|
bde043d13b | ||
|
|
08b465d219 | ||
|
|
663018455e | ||
|
|
ee43b68606 | ||
|
|
ced6d9e430 | ||
|
|
62109cc0a1 | ||
|
|
0b2893a154 | ||
|
|
e13e0c598e | ||
|
|
60ea64210e | ||
|
|
b42b92c13e | ||
|
|
448e6df7e8 | ||
|
|
a0bd65b174 | ||
|
|
c113a7698b | ||
|
|
16592b9f73 | ||
|
|
a49ceee5ae | ||
|
|
ea24a7f3b8 | ||
|
|
158f643c2b | ||
|
|
b24663db93 | ||
|
|
f8aea303c2 | ||
|
|
e14c8ec2c4 | ||
|
|
4ca998e8bc | ||
|
|
05345789dd | ||
|
|
5ee5a71930 | ||
|
|
c9af0f0977 | ||
|
|
e00da518e7 | ||
|
|
1aadd46c18 | ||
|
|
9b4f3f2c11 | ||
|
|
36232afffd | ||
|
|
c864d55f8f | ||
|
|
a1814676b8 | ||
|
|
430975ce96 | ||
|
|
41f87251c7 | ||
|
|
1098812f95 | ||
|
|
dbdba090ee | ||
|
|
52f133dbc2 | ||
|
|
54f95a1d8b | ||
|
|
0b01566282 | ||
|
|
60bc7ffd0c | ||
|
|
c94c6ea45a | ||
|
|
b2dd3be343 | ||
|
|
d758f64a18 | ||
|
|
00af1217ff | ||
|
|
694e2312fd | ||
|
|
69a5db46bd | ||
|
|
d6eade0119 | ||
|
|
8d1cb50048 | ||
|
|
b4b328cf61 | ||
|
|
e47195a334 | ||
|
|
a56607e91f | ||
|
|
4d00f49757 | ||
|
|
83d795dd18 | ||
|
|
a6b9483a5f | ||
|
|
636345bac8 | ||
|
|
79d4953e64 | ||
|
|
d8084559a9 | ||
|
|
60c86848e4 | ||
|
|
ca8050b94e | ||
|
|
47b08a07d8 | ||
|
|
d50fbfe37d | ||
|
|
77c070bc93 | ||
|
|
cc365554c6 | ||
|
|
8b432809b4 | ||
|
|
c6f56ddbd7 | ||
|
|
09c9241b30 | ||
|
|
711bd2bc6d | ||
|
|
2a81cba978 | ||
|
|
6df490942c | ||
|
|
0b8e7bb4f0 | ||
|
|
4acb3b5da3 | ||
|
|
0676fba514 | ||
|
|
7e264ba58e | ||
|
|
64f319ab4e | ||
|
|
ec0ed788fa | ||
|
|
67d1fac6f6 | ||
|
|
af98eed523 | ||
|
|
bfac9e7023 | ||
|
|
346c4bd69a | ||
|
|
a12808e7c3 | ||
|
|
237d5156b6 | ||
|
|
40c39270c0 | ||
|
|
0d6c551694 | ||
|
|
14b42076ff | ||
|
|
3431441180 | ||
|
|
d1f16ffaeb | ||
|
|
f5b18dd7fd | ||
|
|
1cb826ea83 | ||
|
|
9b7feee481 | ||
|
|
7d24904baf | ||
|
|
3f19e137e4 | ||
|
|
bd8f251ad8 | ||
|
|
52a1d63993 | ||
|
|
33f852a641 | ||
|
|
ce0d97b6e5 | ||
|
|
2d20ee2e4e | ||
|
|
c25160d7eb | ||
|
|
f885d7292f | ||
|
|
84ff5760e0 | ||
|
|
3bd7784c1e | ||
|
|
e4244c5fc8 | ||
|
|
161c91e1fd | ||
|
|
b41569d80c | ||
|
|
86c0e11a91 | ||
|
|
2475a0bb19 | ||
|
|
7f9b9e9104 | ||
|
|
aad7571502 | ||
|
|
57100fe78e | ||
|
|
d400dd4167 | ||
|
|
d33cac0a2f |
2
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
2
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
@@ -73,7 +73,7 @@ body:
|
||||
options:
|
||||
- "32"
|
||||
- "33"
|
||||
- "master"
|
||||
- "34 (master)"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@@ -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
.github/workflows/bug-report-labeler.yml
vendored
Normal file
45
.github/workflows/bug-report-labeler.yml
vendored
Normal 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}`);
|
||||
}
|
||||
}
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -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}}"
|
||||
|
||||
6
.github/workflows/cypress.yml
vendored
6
.github/workflows/cypress.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/integration-dav.yml
vendored
2
.github/workflows/integration-dav.yml
vendored
@@ -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'
|
||||
|
||||
|
||||
6
.github/workflows/performance.yml
vendored
6
.github/workflows/performance.yml
vendored
@@ -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
.github/workflows/rector-apply.yml
vendored
1
.github/workflows/rector-apply.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -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: >
|
||||
|
||||
2
.github/workflows/static-code-analysis.yml
vendored
2
.github/workflows/static-code-analysis.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
# Global exclude
|
||||
.editorconfig
|
||||
.envrc
|
||||
.git
|
||||
.git-blame-ignore-revs
|
||||
.gitattributes
|
||||
@@ -11,12 +12,40 @@
|
||||
.gitignore
|
||||
.gitmodules
|
||||
.idea
|
||||
.jshint
|
||||
.l10nignore
|
||||
.mailmap
|
||||
.nextcloudignore
|
||||
.noopenapi
|
||||
.npmignore
|
||||
.php-cs-fixer*
|
||||
.pre-commit-config.yaml
|
||||
.tag
|
||||
.tx
|
||||
CHANGELOG.md
|
||||
CODE_OF_CONDUCT.md
|
||||
COPYING-README
|
||||
DESIGN.md
|
||||
Makefile
|
||||
README.md
|
||||
SECURITY.md
|
||||
codecov.yml
|
||||
cs-fixer
|
||||
csfixer
|
||||
cypress
|
||||
eslint.config.js
|
||||
flake.lock
|
||||
flake.nix
|
||||
openapi-extractor
|
||||
phpunit
|
||||
psalm
|
||||
psalm*.xml
|
||||
rector
|
||||
stylelint.config.js
|
||||
tests
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
window.d.ts
|
||||
|
||||
# Server specific
|
||||
/.devcontainer
|
||||
|
||||
2
3rdparty
2
3rdparty
Submodule 3rdparty updated: 8f97d8cef3...34fdf0b083
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<name>WebDAV</name>
|
||||
<summary>WebDAV endpoint</summary>
|
||||
<description>WebDAV endpoint</description>
|
||||
<version>1.37.0</version>
|
||||
<version>1.38.0</version>
|
||||
<licence>agpl</licence>
|
||||
<author>owncloud.org</author>
|
||||
<namespace>DAV</namespace>
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ $server = $serverFactory->createServer(
|
||||
}
|
||||
|
||||
$share = $authBackend->getShare();
|
||||
$owner = $share->getShareOwner();
|
||||
$isReadable = $share->getPermissions() & Constants::PERMISSION_READ;
|
||||
$fileId = $share->getNodeId();
|
||||
|
||||
@@ -107,7 +106,7 @@ $server = $serverFactory->createServer(
|
||||
Filesystem::logWarningWhenAddingStorageWrapper($previousLog);
|
||||
|
||||
$rootFolder = Server::get(IRootFolder::class);
|
||||
$userFolder = $rootFolder->getUserFolder($owner);
|
||||
$userFolder = $rootFolder->getUserFolder($share->getSharedBy());
|
||||
$node = $userFolder->getFirstNodeById($fileId);
|
||||
if (!$node) {
|
||||
throw new \Sabre\DAV\Exception\NotFound();
|
||||
|
||||
@@ -99,7 +99,6 @@ $server = $serverFactory->createServer(true, $baseuri, $requestUri, $authPlugin,
|
||||
}
|
||||
|
||||
$share = $authBackend->getShare();
|
||||
$owner = $share->getShareOwner();
|
||||
$isReadable = $share->getPermissions() & Constants::PERMISSION_READ;
|
||||
$fileId = $share->getNodeId();
|
||||
|
||||
@@ -135,7 +134,7 @@ $server = $serverFactory->createServer(true, $baseuri, $requestUri, $authPlugin,
|
||||
Filesystem::logWarningWhenAddingStorageWrapper($previousLog);
|
||||
|
||||
$rootFolder = Server::get(IRootFolder::class);
|
||||
$userFolder = $rootFolder->getUserFolder($owner);
|
||||
$userFolder = $rootFolder->getUserFolder($share->getSharedBy());
|
||||
$node = $userFolder->getFirstNodeById($fileId);
|
||||
if (!$node) {
|
||||
throw new NotFound();
|
||||
|
||||
@@ -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',
|
||||
@@ -388,6 +389,7 @@ return array(
|
||||
'OCA\\DAV\\Migration\\Version1034Date20250605132605' => $baseDir . '/../lib/Migration/Version1034Date20250605132605.php',
|
||||
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => $baseDir . '/../lib/Migration/Version1034Date20250813093701.php',
|
||||
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => $baseDir . '/../lib/Migration/Version1036Date20251202000000.php',
|
||||
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => $baseDir . '/../lib/Migration/Version1038Date20260302000000.php',
|
||||
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
|
||||
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
|
||||
'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.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',
|
||||
@@ -403,6 +404,7 @@ class ComposerStaticInitDAV
|
||||
'OCA\\DAV\\Migration\\Version1034Date20250605132605' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250605132605.php',
|
||||
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250813093701.php',
|
||||
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => __DIR__ . '/..' . '/../lib/Migration/Version1036Date20251202000000.php',
|
||||
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260302000000.php',
|
||||
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
|
||||
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
|
||||
'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php',
|
||||
|
||||
@@ -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",
|
||||
@@ -252,6 +253,7 @@ OC.L10N.register(
|
||||
"Completed on %s" : "Erledigt am %s",
|
||||
"Due on %s by %s" : "Fällig am %s von %s",
|
||||
"Due on %s" : "Fällig am %s",
|
||||
"This is an example contact" : "Dies ist ein Beispielkontakt",
|
||||
"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." : "Willkommen bei Nextcloud Calendar!\n\nDies ist ein Beispielereignis – entdecke die Flexibilität der Planung mit Nextcloud Calendar und nimm beliebige Änderungen vor!\n\nMit Nextcloud Calendar kannst du:\n– Ereignisse mühelos erstellen, bearbeiten und verwalten.\n– Mehrere Kalender erstellen und mit Teamkollegen, Freunden oder der Familie teilen.\n– Verfügbarkeit prüfen und deine Termine anderen anzeigen.\n– Nahtlose Integration mit Apps und Geräten über CalDAV.\n– Individuelle Gestaltung: Plane wiederkehrende Ereignisse, passe Benachrichtigungen und andere Einstellungen an.",
|
||||
"Example event - open me!" : "Beispielereignis – öffne mich!",
|
||||
"System Address Book" : "Systemadressbuch",
|
||||
|
||||
@@ -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",
|
||||
@@ -250,6 +251,7 @@
|
||||
"Completed on %s" : "Erledigt am %s",
|
||||
"Due on %s by %s" : "Fällig am %s von %s",
|
||||
"Due on %s" : "Fällig am %s",
|
||||
"This is an example contact" : "Dies ist ein Beispielkontakt",
|
||||
"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." : "Willkommen bei Nextcloud Calendar!\n\nDies ist ein Beispielereignis – entdecke die Flexibilität der Planung mit Nextcloud Calendar und nimm beliebige Änderungen vor!\n\nMit Nextcloud Calendar kannst du:\n– Ereignisse mühelos erstellen, bearbeiten und verwalten.\n– Mehrere Kalender erstellen und mit Teamkollegen, Freunden oder der Familie teilen.\n– Verfügbarkeit prüfen und deine Termine anderen anzeigen.\n– Nahtlose Integration mit Apps und Geräten über CalDAV.\n– Individuelle Gestaltung: Plane wiederkehrende Ereignisse, passe Benachrichtigungen und andere Einstellungen an.",
|
||||
"Example event - open me!" : "Beispielereignis – öffne mich!",
|
||||
"System Address Book" : "Systemadressbuch",
|
||||
|
||||
@@ -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",
|
||||
@@ -252,6 +253,7 @@ OC.L10N.register(
|
||||
"Completed on %s" : "Erledigt am %s",
|
||||
"Due on %s by %s" : "Fällig am %s von %s",
|
||||
"Due on %s" : "Fällig am %s",
|
||||
"This is an example contact" : "Dies ist ein Beispielkontakt",
|
||||
"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." : "Willkommen bei Nextcloud Calendar!\n\nDies ist ein Beispielereignis – entdecken Sie die Flexibilität der Planung mit Nextcloud Calendar und nehmen Sie beliebige Änderungen vor!\n\nMit Nextcloud Calendar können Sie:\n– Ereignisse mühelos erstellen, bearbeiten und verwalten.\n– Mehrere Kalender erstellen und mit Teamkollegen, Freunden oder der Familie teilen.\n– Verfügbarkeit prüfen und Ihre Termine anderen anzeigen.\n– Nahtlose Integration mit Apps und Geräten über CalDAV.\n– Individuelle Gestaltung: Planen Sie wiederkehrende Ereignisse, passen Sie Benachrichtigungen und andere Einstellungen an.",
|
||||
"Example event - open me!" : "Beispielereignis – öffne mich!",
|
||||
"System Address Book" : "Systemadressbuch",
|
||||
|
||||
@@ -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",
|
||||
@@ -250,6 +251,7 @@
|
||||
"Completed on %s" : "Erledigt am %s",
|
||||
"Due on %s by %s" : "Fällig am %s von %s",
|
||||
"Due on %s" : "Fällig am %s",
|
||||
"This is an example contact" : "Dies ist ein Beispielkontakt",
|
||||
"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." : "Willkommen bei Nextcloud Calendar!\n\nDies ist ein Beispielereignis – entdecken Sie die Flexibilität der Planung mit Nextcloud Calendar und nehmen Sie beliebige Änderungen vor!\n\nMit Nextcloud Calendar können Sie:\n– Ereignisse mühelos erstellen, bearbeiten und verwalten.\n– Mehrere Kalender erstellen und mit Teamkollegen, Freunden oder der Familie teilen.\n– Verfügbarkeit prüfen und Ihre Termine anderen anzeigen.\n– Nahtlose Integration mit Apps und Geräten über CalDAV.\n– Individuelle Gestaltung: Planen Sie wiederkehrende Ereignisse, passen Sie Benachrichtigungen und andere Einstellungen an.",
|
||||
"Example event - open me!" : "Beispielereignis – öffne mich!",
|
||||
"System Address Book" : "Systemadressbuch",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -236,6 +236,7 @@ OC.L10N.register(
|
||||
"Failed to check file size: %1$s" : "Faili suuruse kontrollimine ei õnnestunud: %1$s",
|
||||
"Could not open file: %1$s (%2$d), file does seem to exist" : "„%1$s“ (%2$d) faili avamine ei õnnestunud, aga tundub, et ta on olemas",
|
||||
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "„%1$s“ (%2$d) faili avamine ei õnnestunud, tundub, et teda pole olemas",
|
||||
"Failed to get size for : %1$s" : "Suuruse tuvastamine ei õnnestunud: %1$s",
|
||||
"Encryption not ready: %1$s" : "Krüptimine pole veel kasutatav: %1$s",
|
||||
"Failed to open file: %1$s" : "Faili avamine ei õnnestunud: %1$s",
|
||||
"Failed to unlink: %1$s" : "Lingi eemaldamine ei õnnestunud: %1$s",
|
||||
@@ -252,18 +253,19 @@ OC.L10N.register(
|
||||
"Completed on %s" : "Lõpetatud %s",
|
||||
"Due on %s by %s" : "Tähtaeg: %s, täitjaks %s",
|
||||
"Due on %s" : "Tähtaeg: %s",
|
||||
"This is an example contact" : "See on näidiskontakt",
|
||||
"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.",
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"Failed to check file size: %1$s" : "Faili suuruse kontrollimine ei õnnestunud: %1$s",
|
||||
"Could not open file: %1$s (%2$d), file does seem to exist" : "„%1$s“ (%2$d) faili avamine ei õnnestunud, aga tundub, et ta on olemas",
|
||||
"Could not open file: %1$s (%2$d), file doesn't seem to exist" : "„%1$s“ (%2$d) faili avamine ei õnnestunud, tundub, et teda pole olemas",
|
||||
"Failed to get size for : %1$s" : "Suuruse tuvastamine ei õnnestunud: %1$s",
|
||||
"Encryption not ready: %1$s" : "Krüptimine pole veel kasutatav: %1$s",
|
||||
"Failed to open file: %1$s" : "Faili avamine ei õnnestunud: %1$s",
|
||||
"Failed to unlink: %1$s" : "Lingi eemaldamine ei õnnestunud: %1$s",
|
||||
@@ -250,18 +251,19 @@
|
||||
"Completed on %s" : "Lõpetatud %s",
|
||||
"Due on %s by %s" : "Tähtaeg: %s, täitjaks %s",
|
||||
"Due on %s" : "Tähtaeg: %s",
|
||||
"This is an example contact" : "See on näidiskontakt",
|
||||
"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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
@@ -252,6 +253,7 @@ OC.L10N.register(
|
||||
"Completed on %s" : "Concluída em %s",
|
||||
"Due on %s by %s" : "Vence em %s até %s",
|
||||
"Due on %s" : "Vence em %s",
|
||||
"This is an example contact" : "Este é um exemplo de contato",
|
||||
"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." : "Bem-vindo ao Nextcloud Calendário!\n\nEste é um exemplo de evento - explore a flexibilidade do planejamento com o Nextcloud Calendário fazendo as edições que desejar!\n\nCom o Nextcloud Calendário, você pode:\n- Criar, editar e gerenciar eventos sem esforço.\n- Criar vários calendários e compartilhá-los com colegas de equipe, amigos ou familiares.\n- Verificar a disponibilidade e exibir seus horários de trabalho para outras pessoas.\n- Integrá-lo perfeitamente com aplicativos e dispositivos via CalDAV.\n- Personalizar sua experiência: agende eventos recorrentes, ajuste as notificações e outras configurações.",
|
||||
"Example event - open me!" : "Exemplo de evento - abra-me!",
|
||||
"System Address Book" : "Catálogo de Endereços do Sistema",
|
||||
|
||||
@@ -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",
|
||||
@@ -250,6 +251,7 @@
|
||||
"Completed on %s" : "Concluída em %s",
|
||||
"Due on %s by %s" : "Vence em %s até %s",
|
||||
"Due on %s" : "Vence em %s",
|
||||
"This is an example contact" : "Este é um exemplo de contato",
|
||||
"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." : "Bem-vindo ao Nextcloud Calendário!\n\nEste é um exemplo de evento - explore a flexibilidade do planejamento com o Nextcloud Calendário fazendo as edições que desejar!\n\nCom o Nextcloud Calendário, você pode:\n- Criar, editar e gerenciar eventos sem esforço.\n- Criar vários calendários e compartilhá-los com colegas de equipe, amigos ou familiares.\n- Verificar a disponibilidade e exibir seus horários de trabalho para outras pessoas.\n- Integrá-lo perfeitamente com aplicativos e dispositivos via CalDAV.\n- Personalizar sua experiência: agende eventos recorrentes, ajuste as notificações e outras configurações.",
|
||||
"Example event - open me!" : "Exemplo de evento - abra-me!",
|
||||
"System Address Book" : "Catálogo de Endereços do Sistema",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -191,6 +191,7 @@ OC.L10N.register(
|
||||
"{actor} updated contact {card} in address book {addressbook}" : "{actor} оновив(-ла) контакт {card} в адресній книзі {addressbook}",
|
||||
"You updated contact {card} in address book {addressbook}" : "Ви оновили контакт {card} в адресній книзі {addressbook}",
|
||||
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Контактну</strong> або <strong>адресну книгу</strong> було змінено",
|
||||
"System address book disabled" : "Системну адресну книгу вимкнено",
|
||||
"Accounts" : "Облікові записи",
|
||||
"System address book which holds all accounts" : "Системна адресна книга, в якій містяться всі облікові записи",
|
||||
"File is not updatable: %1$s" : "Файл не оновлюється: %1$s",
|
||||
@@ -203,6 +204,8 @@ OC.L10N.register(
|
||||
"Could not rename part file to final file, canceled by hook" : "Не вдалося перейменувати файл частини на остаточний файл, скасовано підхопленням",
|
||||
"Could not rename part file to final file" : "Не вдалося перейменувати файл частини на остаточний файл",
|
||||
"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), схоже, що файл відсутній",
|
||||
"Encryption not ready: %1$s" : "Шифрування не готове: %1$s",
|
||||
"Failed to open file: %1$s" : "Не вдалося відкрити файл: %1$s",
|
||||
"Failed to unlink: %1$s" : "Не вдалося від’єднати: %1$s",
|
||||
@@ -227,6 +230,10 @@ OC.L10N.register(
|
||||
"DAV system address book" : "Системна адресна книга DAV",
|
||||
"No outstanding DAV system address book sync." : "Немає незавершеної синхронізації системної адресної книги DAV.",
|
||||
"The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронізація системної адресної книги DAV ще не запускалася, оскільки, або ваша система вже має понад 1000 користувачів, або сталася помилка. Будь ласка, запустіть синхронізацію вручну за допомогою команди \"occ dav:sync-system-addressbook\".",
|
||||
"DAV system address book size" : "Розмір системної адресної книги DAV ",
|
||||
"The system address book is disabled" : "Системну адресну книгу вимкнено",
|
||||
"The system address book is enabled, but contains more than the configured limit of %d contacts" : "Системну адресну книгу увімкнено, проте вона містить більше, ніж визначено максимальну кількість у %d контактів.",
|
||||
"The system address book is enabled and contains less than the configured limit of %d contacts" : "Системну адресну книгу увімкнено, проте вона містить менше, ніж визначено максимальну кількість у %d контактів.",
|
||||
"WebDAV endpoint" : "Точка доступу WebDAV",
|
||||
"Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Неможливо перевірити, чи на вашому вебсервері правильно налаштовано доступ для синхронізації файлів через протокол WebDAV. Перевірте це вручну.",
|
||||
"Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Ваш вебсервер не налаштований як треба для синхронізації файлів, схоже інтерфейс WebDAV поламаний.",
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
"{actor} updated contact {card} in address book {addressbook}" : "{actor} оновив(-ла) контакт {card} в адресній книзі {addressbook}",
|
||||
"You updated contact {card} in address book {addressbook}" : "Ви оновили контакт {card} в адресній книзі {addressbook}",
|
||||
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "<strong>Контактну</strong> або <strong>адресну книгу</strong> було змінено",
|
||||
"System address book disabled" : "Системну адресну книгу вимкнено",
|
||||
"Accounts" : "Облікові записи",
|
||||
"System address book which holds all accounts" : "Системна адресна книга, в якій містяться всі облікові записи",
|
||||
"File is not updatable: %1$s" : "Файл не оновлюється: %1$s",
|
||||
@@ -201,6 +202,8 @@
|
||||
"Could not rename part file to final file, canceled by hook" : "Не вдалося перейменувати файл частини на остаточний файл, скасовано підхопленням",
|
||||
"Could not rename part file to final file" : "Не вдалося перейменувати файл частини на остаточний файл",
|
||||
"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), схоже, що файл відсутній",
|
||||
"Encryption not ready: %1$s" : "Шифрування не готове: %1$s",
|
||||
"Failed to open file: %1$s" : "Не вдалося відкрити файл: %1$s",
|
||||
"Failed to unlink: %1$s" : "Не вдалося від’єднати: %1$s",
|
||||
@@ -225,6 +228,10 @@
|
||||
"DAV system address book" : "Системна адресна книга DAV",
|
||||
"No outstanding DAV system address book sync." : "Немає незавершеної синхронізації системної адресної книги DAV.",
|
||||
"The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "Синхронізація системної адресної книги DAV ще не запускалася, оскільки, або ваша система вже має понад 1000 користувачів, або сталася помилка. Будь ласка, запустіть синхронізацію вручну за допомогою команди \"occ dav:sync-system-addressbook\".",
|
||||
"DAV system address book size" : "Розмір системної адресної книги DAV ",
|
||||
"The system address book is disabled" : "Системну адресну книгу вимкнено",
|
||||
"The system address book is enabled, but contains more than the configured limit of %d contacts" : "Системну адресну книгу увімкнено, проте вона містить більше, ніж визначено максимальну кількість у %d контактів.",
|
||||
"The system address book is enabled and contains less than the configured limit of %d contacts" : "Системну адресну книгу увімкнено, проте вона містить менше, ніж визначено максимальну кількість у %d контактів.",
|
||||
"WebDAV endpoint" : "Точка доступу WebDAV",
|
||||
"Could not check that your web server is properly set up to allow file synchronization over WebDAV. Please check manually." : "Неможливо перевірити, чи на вашому вебсервері правильно налаштовано доступ для синхронізації файлів через протокол WebDAV. Перевірте це вручну.",
|
||||
"Your web server is not yet properly set up to allow file synchronization, because the WebDAV interface seems to be broken." : "Ваш вебсервер не налаштований як треба для синхронізації файлів, схоже інтерфейс WebDAV поламаний.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
@@ -252,6 +253,7 @@ OC.L10N.register(
|
||||
"Completed on %s" : "完成於 %s",
|
||||
"Due on %s by %s" : "到期於 %s 由 %s",
|
||||
"Due on %s" : "到期於 %s",
|
||||
"This is an example contact" : "此為範例聯絡人",
|
||||
"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." : "歡迎使用 Nextcloud 日曆!\n\n這是範例事件 - 使用 Nextcloud 日曆進行任何編輯,探索規劃的彈性!\n\n使用 Nextcloud 日曆,您可以:\n- 毫不費力地建立、編輯與管理活動。\n- 建立多個日曆,並與同事、朋友或家人分享。\n- 檢查可得性,並向他人顯示您的忙碌時間。\n- 透過 CalDAV 與應用程式與裝置無縫整合。\n- 自訂您的體驗:排定定期活動、調整通知與其他設定。",
|
||||
"Example event - open me!" : "範例活動 - 開啟我!",
|
||||
"System Address Book" : "系統通訊錄",
|
||||
|
||||
@@ -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",
|
||||
@@ -250,6 +251,7 @@
|
||||
"Completed on %s" : "完成於 %s",
|
||||
"Due on %s by %s" : "到期於 %s 由 %s",
|
||||
"Due on %s" : "到期於 %s",
|
||||
"This is an example contact" : "此為範例聯絡人",
|
||||
"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." : "歡迎使用 Nextcloud 日曆!\n\n這是範例事件 - 使用 Nextcloud 日曆進行任何編輯,探索規劃的彈性!\n\n使用 Nextcloud 日曆,您可以:\n- 毫不費力地建立、編輯與管理活動。\n- 建立多個日曆,並與同事、朋友或家人分享。\n- 檢查可得性,並向他人顯示您的忙碌時間。\n- 透過 CalDAV 與應用程式與裝置無縫整合。\n- 自訂您的體驗:排定定期活動、調整通知與其他設定。",
|
||||
"Example event - open me!" : "範例活動 - 開啟我!",
|
||||
"System Address Book" : "系統通訊錄",
|
||||
|
||||
@@ -151,6 +151,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
|
||||
'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
|
||||
'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm' => ['default_alarm', 'int'],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -325,7 +325,7 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIs
|
||||
public function export(?CalendarExportOptions $options = null): Generator {
|
||||
foreach (
|
||||
$this->backend->exportCalendar(
|
||||
$this->calendarInfo['id'],
|
||||
(int)$this->calendarInfo['id'],
|
||||
$this->backend::CALENDAR_TYPE_CALENDAR,
|
||||
$options
|
||||
) as $event
|
||||
|
||||
@@ -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 {
|
||||
|
||||
107
apps/dav/lib/CalDAV/Federation/FederatedCalendarObject.php
Normal file
107
apps/dav/lib/CalDAV/Federation/FederatedCalendarObject.php
Normal file
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -8,7 +8,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace OCA\DAV\CardDAV\Notification;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCP\IL10N;
|
||||
use OCP\L10N\IFactory;
|
||||
@@ -42,7 +41,7 @@ class Notifier implements INotifier {
|
||||
*/
|
||||
public function prepare(INotification $notification, string $languageCode): INotification {
|
||||
if ($notification->getApp() !== Application::APP_ID) {
|
||||
throw new InvalidArgumentException();
|
||||
throw new UnknownNotificationException();
|
||||
}
|
||||
|
||||
$l = $this->l10nFactory->get(Application::APP_ID, $languageCode);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -480,11 +480,16 @@ 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.
|
||||
if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
|
||||
$logger = Server::get(LoggerInterface::class);
|
||||
$logger->warning('fixing cached size of file id=' . $this->getId());
|
||||
//
|
||||
$fsSize = $this->fileView->filesize($this->getPath());
|
||||
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());
|
||||
$this->refreshInfo();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class PublicLinkCheckPlugin extends ServerPlugin {
|
||||
}
|
||||
|
||||
public function beforeMethod(RequestInterface $request, ResponseInterface $response) {
|
||||
// verify that the owner didn't have their share permissions revoked
|
||||
// verify that the initiator didn't have their share permissions revoked
|
||||
if ($this->fileInfo && !$this->fileInfo->isShareable()) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
37
apps/dav/lib/Migration/Version1038Date20260302000000.php
Normal file
37
apps/dav/lib/Migration/Version1038Date20260302000000.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\Attributes\AddColumn;
|
||||
use OCP\Migration\Attributes\ColumnType;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
#[AddColumn(table: 'calendars', name: 'default_alarm', type: ColumnType::STRING)]
|
||||
class Version1038Date20260302000000 extends SimpleMigrationStep {
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
$calendarsTable = $schema->getTable('calendars');
|
||||
|
||||
if (!$calendarsTable->hasColumn('default_alarm')) {
|
||||
$calendarsTable->addColumn('default_alarm', Types::INTEGER, [
|
||||
'notnull' => false,
|
||||
'default' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
||||
namespace OCA\DAV\Provisioning\Apple;
|
||||
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Constants;
|
||||
use Sabre\DAV\Exception\Forbidden;
|
||||
use Sabre\DAV\INode;
|
||||
use Sabre\DAV\IProperties;
|
||||
@@ -60,7 +61,7 @@ class AppleProvisioningNode implements INode, IProperties {
|
||||
|
||||
return [
|
||||
'{DAV:}getcontentlength' => 42,
|
||||
'{DAV:}getlastmodified' => $datetime->format(\DateTimeInterface::RFC7231),
|
||||
'{DAV:}getlastmodified' => $datetime->format(Constants::DATE_RFC7231),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,452 +11,57 @@ namespace OCA\DAV\UserMigration;
|
||||
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin;
|
||||
use OCA\DAV\CalDAV\Plugin as CalDAVPlugin;
|
||||
use OCA\DAV\Connector\Sabre\CachingTree;
|
||||
use OCA\DAV\Connector\Sabre\Server as SabreDavServer;
|
||||
use OCA\DAV\RootCollection;
|
||||
use OCP\Calendar\ICalendar;
|
||||
use OCA\DAV\CalDAV\CalendarImpl;
|
||||
use OCA\DAV\CalDAV\Export\ExportService;
|
||||
use OCA\DAV\CalDAV\Import\ImportService;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Calendar\CalendarExportOptions;
|
||||
use OCP\Calendar\CalendarImportOptions;
|
||||
use OCP\Calendar\IManager as ICalendarManager;
|
||||
use OCP\Defaults;
|
||||
use OCP\IL10N;
|
||||
use OCP\ITempManager;
|
||||
use OCP\IUser;
|
||||
use OCP\UserMigration\IExportDestination;
|
||||
use OCP\UserMigration\IImportSource;
|
||||
use OCP\UserMigration\IMigrator;
|
||||
use OCP\UserMigration\ISizeEstimationMigrator;
|
||||
use OCP\UserMigration\TMigratorBasicVersionHandling;
|
||||
use Sabre\VObject\Component as VObjectComponent;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VTimeZone;
|
||||
use Sabre\VObject\Property\ICalendar\DateTime;
|
||||
use Sabre\VObject\Reader as VObjectReader;
|
||||
use Sabre\VObject\UUIDUtil;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Sabre\DAV\Xml\Property\Href;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Throwable;
|
||||
use function substr;
|
||||
|
||||
class CalendarMigrator implements IMigrator, ISizeEstimationMigrator {
|
||||
|
||||
use TMigratorBasicVersionHandling;
|
||||
|
||||
private SabreDavServer $sabreDavServer;
|
||||
|
||||
private const PATH_ROOT = Application::APP_ID . '/calendars/';
|
||||
private const PATH_VERSION = self::PATH_ROOT . 'version.json';
|
||||
private const PATH_CALENDARS = self::PATH_ROOT . 'calendars.json';
|
||||
private const PATH_SUBSCRIPTIONS = self::PATH_ROOT . 'subscriptions.json';
|
||||
private const USERS_URI_ROOT = 'principals/users/';
|
||||
|
||||
private const FILENAME_EXT = '.ics';
|
||||
|
||||
private const MIGRATED_URI_PREFIX = 'migrated-';
|
||||
|
||||
private const EXPORT_ROOT = Application::APP_ID . '/calendars/';
|
||||
private const DAV_PROPERTY_URI = 'uri';
|
||||
private const DAV_PROPERTY_DISPLAYNAME = '{DAV:}displayname';
|
||||
private const DAV_PROPERTY_CALENDAR_COLOR = '{http://apple.com/ns/ical/}calendar-color';
|
||||
private const DAV_PROPERTY_CALENDAR_TIMEZONE = '{urn:ietf:params:xml:ns:caldav}calendar-timezone';
|
||||
private const DAV_PROPERTY_SUBSCRIBED_SOURCE = 'source';
|
||||
private const DAV_PROPERTY_SUBSCRIBED_STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos';
|
||||
private const DAV_PROPERTY_SUBSCRIBED_STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms';
|
||||
private const DAV_PROPERTY_SUBSCRIBED_STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments';
|
||||
|
||||
public function __construct(
|
||||
private CalDavBackend $calDavBackend,
|
||||
private ICalendarManager $calendarManager,
|
||||
private ICSExportPlugin $icsExportPlugin,
|
||||
private Defaults $defaults,
|
||||
private IL10N $l10n,
|
||||
private readonly IAppManager $appManager,
|
||||
private readonly CalDavBackend $calDavBackend,
|
||||
private readonly ICalendarManager $calendarManager,
|
||||
private readonly Defaults $defaults,
|
||||
private readonly IL10N $l10n,
|
||||
private readonly ExportService $exportService,
|
||||
private readonly ImportService $importService,
|
||||
private readonly ITempManager $tempManager,
|
||||
) {
|
||||
$root = new RootCollection();
|
||||
$this->sabreDavServer = new SabreDavServer(new CachingTree($root));
|
||||
$this->sabreDavServer->addPlugin(new CalDAVPlugin());
|
||||
}
|
||||
|
||||
private function getPrincipalUri(IUser $user): string {
|
||||
return CalendarMigrator::USERS_URI_ROOT . $user->getUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{name: string, vCalendar: VCalendar}
|
||||
*
|
||||
* @throws CalendarMigratorException
|
||||
* @throws InvalidCalendarException
|
||||
*/
|
||||
private function getCalendarExportData(IUser $user, ICalendar $calendar, OutputInterface $output): array {
|
||||
$userId = $user->getUID();
|
||||
$uri = $calendar->getUri();
|
||||
$path = CalDAVPlugin::CALENDAR_ROOT . "/$userId/$uri";
|
||||
|
||||
/**
|
||||
* @see \Sabre\CalDAV\ICSExportPlugin::httpGet() implementation reference
|
||||
*/
|
||||
|
||||
$properties = $this->sabreDavServer->getProperties($path, [
|
||||
'{DAV:}resourcetype',
|
||||
'{DAV:}displayname',
|
||||
'{http://sabredav.org/ns}sync-token',
|
||||
'{DAV:}sync-token',
|
||||
'{http://apple.com/ns/ical/}calendar-color',
|
||||
]);
|
||||
|
||||
// Filter out invalid (e.g. deleted) calendars
|
||||
if (!isset($properties['{DAV:}resourcetype']) || !$properties['{DAV:}resourcetype']->is('{' . CalDAVPlugin::NS_CALDAV . '}calendar')) {
|
||||
throw new InvalidCalendarException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see \Sabre\CalDAV\ICSExportPlugin::generateResponse() implementation reference
|
||||
*/
|
||||
|
||||
$calDataProp = '{' . CalDAVPlugin::NS_CALDAV . '}calendar-data';
|
||||
$calendarNode = $this->sabreDavServer->tree->getNodeForPath($path);
|
||||
$nodes = $this->sabreDavServer->getPropertiesIteratorForPath($path, [$calDataProp], 1);
|
||||
|
||||
$blobs = [];
|
||||
foreach ($nodes as $node) {
|
||||
if (isset($node[200][$calDataProp])) {
|
||||
$blobs[$node['href']] = $node[200][$calDataProp];
|
||||
}
|
||||
}
|
||||
|
||||
$mergedCalendar = $this->icsExportPlugin->mergeObjects(
|
||||
$properties,
|
||||
$blobs,
|
||||
);
|
||||
|
||||
$problems = $mergedCalendar->validate();
|
||||
if (!empty($problems)) {
|
||||
$output->writeln('Skipping calendar "' . $properties['{DAV:}displayname'] . '" containing invalid calendar data');
|
||||
throw new InvalidCalendarException();
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $calendarNode->getName(),
|
||||
'vCalendar' => $mergedCalendar,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{name: string, vCalendar: VCalendar}>
|
||||
*
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
private function getCalendarExports(IUser $user, OutputInterface $output): array {
|
||||
$principalUri = $this->getPrincipalUri($user);
|
||||
|
||||
return array_values(array_filter(array_map(
|
||||
function (ICalendar $calendar) use ($user, $output) {
|
||||
try {
|
||||
return $this->getCalendarExportData($user, $calendar, $output);
|
||||
} catch (InvalidCalendarException $e) {
|
||||
// Allow this exception as invalid (e.g. deleted) calendars are not to be exported
|
||||
return null;
|
||||
}
|
||||
},
|
||||
$this->calendarManager->getCalendarsForPrincipal($principalUri),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidCalendarException
|
||||
*/
|
||||
private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): string {
|
||||
$principalUri = $this->getPrincipalUri($user);
|
||||
|
||||
$initialCalendarUri = substr($initialCalendarUri, 0, strlen(CalendarMigrator::MIGRATED_URI_PREFIX)) === CalendarMigrator::MIGRATED_URI_PREFIX
|
||||
? $initialCalendarUri
|
||||
: CalendarMigrator::MIGRATED_URI_PREFIX . $initialCalendarUri;
|
||||
|
||||
if ($initialCalendarUri === '') {
|
||||
throw new InvalidCalendarException();
|
||||
}
|
||||
|
||||
$existingCalendarUris = array_map(
|
||||
fn (ICalendar $calendar) => $calendar->getUri(),
|
||||
$this->calendarManager->getCalendarsForPrincipal($principalUri),
|
||||
);
|
||||
|
||||
$calendarUri = $initialCalendarUri;
|
||||
$acc = 1;
|
||||
while (in_array($calendarUri, $existingCalendarUris, true)) {
|
||||
$calendarUri = $initialCalendarUri . "-$acc";
|
||||
++$acc;
|
||||
}
|
||||
|
||||
return $calendarUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getEstimatedExportSize(IUser $user): int|float {
|
||||
$calendarExports = $this->getCalendarExports($user, new NullOutput());
|
||||
$calendarCount = count($calendarExports);
|
||||
|
||||
// 150B for top-level properties
|
||||
$size = ($calendarCount * 150) / 1024;
|
||||
|
||||
$componentCount = array_sum(array_map(
|
||||
function (array $data): int {
|
||||
/** @var VCalendar $vCalendar */
|
||||
$vCalendar = $data['vCalendar'];
|
||||
return count($vCalendar->getComponents());
|
||||
},
|
||||
$calendarExports,
|
||||
));
|
||||
|
||||
// 450B for each component (events, todos, alarms, etc.)
|
||||
$size += ($componentCount * 450) / 1024;
|
||||
|
||||
return ceil($size);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
|
||||
$output->writeln('Exporting calendars into ' . CalendarMigrator::EXPORT_ROOT . '…');
|
||||
|
||||
$calendarExports = $this->getCalendarExports($user, $output);
|
||||
|
||||
if (empty($calendarExports)) {
|
||||
$output->writeln('No calendars to export…');
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* @var string $name
|
||||
* @var VCalendar $vCalendar
|
||||
*/
|
||||
foreach ($calendarExports as ['name' => $name, 'vCalendar' => $vCalendar]) {
|
||||
// Set filename to sanitized calendar name
|
||||
$filename = preg_replace('/[^a-z0-9-_]/iu', '', $name) . CalendarMigrator::FILENAME_EXT;
|
||||
$exportPath = CalendarMigrator::EXPORT_ROOT . $filename;
|
||||
|
||||
$exportDestination->addFileContents($exportPath, $vCalendar->serialize());
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
throw new CalendarMigratorException('Could not export calendars', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, VTimeZone>
|
||||
*/
|
||||
private function getCalendarTimezones(VCalendar $vCalendar): array {
|
||||
/** @var VTimeZone[] $calendarTimezones */
|
||||
$calendarTimezones = array_filter(
|
||||
$vCalendar->getComponents(),
|
||||
fn ($component) => $component->name === 'VTIMEZONE',
|
||||
);
|
||||
|
||||
/** @var array<string, VTimeZone> $calendarTimezoneMap */
|
||||
$calendarTimezoneMap = [];
|
||||
foreach ($calendarTimezones as $vTimeZone) {
|
||||
$calendarTimezoneMap[$vTimeZone->getTimeZone()->getName()] = $vTimeZone;
|
||||
}
|
||||
|
||||
return $calendarTimezoneMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VTimeZone[]
|
||||
*/
|
||||
private function getTimezonesForComponent(VCalendar $vCalendar, VObjectComponent $component): array {
|
||||
$componentTimezoneIds = [];
|
||||
|
||||
foreach ($component->children() as $child) {
|
||||
if ($child instanceof DateTime && isset($child->parameters['TZID'])) {
|
||||
$timezoneId = $child->parameters['TZID']->getValue();
|
||||
if (!in_array($timezoneId, $componentTimezoneIds, true)) {
|
||||
$componentTimezoneIds[] = $timezoneId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$calendarTimezoneMap = $this->getCalendarTimezones($vCalendar);
|
||||
|
||||
return array_values(array_filter(array_map(
|
||||
fn (string $timezoneId) => $calendarTimezoneMap[$timezoneId],
|
||||
$componentTimezoneIds,
|
||||
)));
|
||||
}
|
||||
|
||||
private function sanitizeComponent(VObjectComponent $component): VObjectComponent {
|
||||
// Operate on the component clone to prevent mutation of the original
|
||||
$component = clone $component;
|
||||
|
||||
// Remove RSVP parameters to prevent automatically sending invitation emails to attendees on import
|
||||
foreach ($component->children() as $child) {
|
||||
if (
|
||||
$child->name === 'ATTENDEE'
|
||||
&& isset($child->parameters['RSVP'])
|
||||
) {
|
||||
unset($child->parameters['RSVP']);
|
||||
}
|
||||
}
|
||||
|
||||
return $component;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VObjectComponent[]
|
||||
*/
|
||||
private function getRequiredImportComponents(VCalendar $vCalendar, VObjectComponent $component): array {
|
||||
$component = $this->sanitizeComponent($component);
|
||||
/** @var array<int, VTimeZone> $timezoneComponents */
|
||||
$timezoneComponents = $this->getTimezonesForComponent($vCalendar, $component);
|
||||
return [
|
||||
...$timezoneComponents,
|
||||
$component,
|
||||
];
|
||||
}
|
||||
|
||||
private function initCalendarObject(): VCalendar {
|
||||
$vCalendarObject = new VCalendar();
|
||||
$vCalendarObject->PRODID = '-//IDN nextcloud.com//Migrated calendar//EN';
|
||||
return $vCalendarObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidCalendarException
|
||||
*/
|
||||
private function importCalendarObject(int $calendarId, VCalendar $vCalendarObject, string $filename, OutputInterface $output): void {
|
||||
try {
|
||||
$this->calDavBackend->createCalendarObject(
|
||||
$calendarId,
|
||||
UUIDUtil::getUUID() . CalendarMigrator::FILENAME_EXT,
|
||||
$vCalendarObject->serialize(),
|
||||
CalDavBackend::CALENDAR_TYPE_CALENDAR,
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln("Error creating calendar object, rolling back creation of \"$filename\" calendar…");
|
||||
$this->calDavBackend->deleteCalendar($calendarId, true);
|
||||
throw new InvalidCalendarException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidCalendarException
|
||||
*/
|
||||
private function importCalendar(IUser $user, string $filename, string $initialCalendarUri, VCalendar $vCalendar, OutputInterface $output): void {
|
||||
$principalUri = $this->getPrincipalUri($user);
|
||||
$calendarUri = $this->getUniqueCalendarUri($user, $initialCalendarUri);
|
||||
|
||||
$calendarId = $this->calDavBackend->createCalendar($principalUri, $calendarUri, [
|
||||
'{DAV:}displayname' => isset($vCalendar->{'X-WR-CALNAME'}) ? $vCalendar->{'X-WR-CALNAME'}->getValue() : $this->l10n->t('Migrated calendar (%1$s)', [$filename]),
|
||||
'{http://apple.com/ns/ical/}calendar-color' => isset($vCalendar->{'X-APPLE-CALENDAR-COLOR'}) ? $vCalendar->{'X-APPLE-CALENDAR-COLOR'}->getValue() : $this->defaults->getColorPrimary(),
|
||||
'components' => implode(
|
||||
',',
|
||||
array_reduce(
|
||||
$vCalendar->getComponents(),
|
||||
function (array $componentNames, VObjectComponent $component) {
|
||||
/** @var array<int, string> $componentNames */
|
||||
return !in_array($component->name, $componentNames, true)
|
||||
? [...$componentNames, $component->name]
|
||||
: $componentNames;
|
||||
},
|
||||
[],
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
/** @var VObjectComponent[] $calendarComponents */
|
||||
$calendarComponents = array_values(array_filter(
|
||||
$vCalendar->getComponents(),
|
||||
// VTIMEZONE components are handled separately and added to the calendar object only if depended on by the component
|
||||
fn (VObjectComponent $component) => $component->name !== 'VTIMEZONE',
|
||||
));
|
||||
|
||||
/** @var array<string, VObjectComponent[]> $groupedCalendarComponents */
|
||||
$groupedCalendarComponents = [];
|
||||
/** @var VObjectComponent[] $ungroupedCalendarComponents */
|
||||
$ungroupedCalendarComponents = [];
|
||||
|
||||
foreach ($calendarComponents as $component) {
|
||||
if (isset($component->UID)) {
|
||||
$uid = $component->UID->getValue();
|
||||
// Components with the same UID (e.g. recurring events) are grouped together into a single calendar object
|
||||
if (isset($groupedCalendarComponents[$uid])) {
|
||||
$groupedCalendarComponents[$uid][] = $component;
|
||||
} else {
|
||||
$groupedCalendarComponents[$uid] = [$component];
|
||||
}
|
||||
} else {
|
||||
$ungroupedCalendarComponents[] = $component;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($groupedCalendarComponents as $uid => $components) {
|
||||
// Construct and import a calendar object containing all components of a group
|
||||
$vCalendarObject = $this->initCalendarObject();
|
||||
foreach ($components as $component) {
|
||||
foreach ($this->getRequiredImportComponents($vCalendar, $component) as $component) {
|
||||
$vCalendarObject->add($component);
|
||||
}
|
||||
}
|
||||
$this->importCalendarObject($calendarId, $vCalendarObject, $filename, $output);
|
||||
}
|
||||
|
||||
foreach ($ungroupedCalendarComponents as $component) {
|
||||
// Construct and import a calendar object for a single component
|
||||
$vCalendarObject = $this->initCalendarObject();
|
||||
foreach ($this->getRequiredImportComponents($vCalendar, $component) as $component) {
|
||||
$vCalendarObject->add($component);
|
||||
}
|
||||
$this->importCalendarObject($calendarId, $vCalendarObject, $filename, $output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
public function import(IUser $user, IImportSource $importSource, OutputInterface $output): void {
|
||||
if ($importSource->getMigratorVersion($this->getId()) === null) {
|
||||
$output->writeln('No version for ' . static::class . ', skipping import…');
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln('Importing calendars from ' . CalendarMigrator::EXPORT_ROOT . '…');
|
||||
|
||||
$calendarImports = $importSource->getFolderListing(CalendarMigrator::EXPORT_ROOT);
|
||||
if (empty($calendarImports)) {
|
||||
$output->writeln('No calendars to import…');
|
||||
}
|
||||
|
||||
foreach ($calendarImports as $filename) {
|
||||
$importPath = CalendarMigrator::EXPORT_ROOT . $filename;
|
||||
try {
|
||||
/** @var VCalendar $vCalendar */
|
||||
$vCalendar = VObjectReader::read(
|
||||
$importSource->getFileAsStream($importPath),
|
||||
VObjectReader::OPTION_FORGIVING,
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln("Failed to read file \"$importPath\", skipping…");
|
||||
continue;
|
||||
}
|
||||
|
||||
$problems = $vCalendar->validate();
|
||||
if (!empty($problems)) {
|
||||
$output->writeln("Invalid calendar data contained in \"$importPath\", skipping…");
|
||||
continue;
|
||||
}
|
||||
|
||||
$splitFilename = explode('.', $filename, 2);
|
||||
if (count($splitFilename) !== 2) {
|
||||
$output->writeln("Invalid filename \"$filename\", expected filename of the format \"<calendar_name>" . CalendarMigrator::FILENAME_EXT . '", skipping…');
|
||||
continue;
|
||||
}
|
||||
[$initialCalendarUri, $ext] = $splitFilename;
|
||||
|
||||
try {
|
||||
$this->importCalendar(
|
||||
$user,
|
||||
$filename,
|
||||
$initialCalendarUri,
|
||||
$vCalendar,
|
||||
$output,
|
||||
);
|
||||
} catch (InvalidCalendarException $e) {
|
||||
// Allow this exception to skip a failed import
|
||||
} finally {
|
||||
$vCalendar->destroy();
|
||||
}
|
||||
}
|
||||
$this->version = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,4 +84,448 @@ class CalendarMigrator implements IMigrator, ISizeEstimationMigrator {
|
||||
public function getDescription(): string {
|
||||
return $this->l10n->t('Calendars including events, details and attendees');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getEstimatedExportSize(IUser $user): int|float {
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
$calendars = $this->calendarManager->getCalendarsForPrincipal($principalUri);
|
||||
|
||||
$calendarCount = 0;
|
||||
$totalSize = 0;
|
||||
|
||||
foreach ($calendars as $calendar) {
|
||||
if (!$calendar instanceof CalendarImpl) {
|
||||
continue;
|
||||
}
|
||||
if ($calendar->isShared()) {
|
||||
continue;
|
||||
}
|
||||
$calendarCount++;
|
||||
// Note: 'uid' is required because getLimitedCalendarObjects uses it as the array key
|
||||
$objects = $this->calDavBackend->getLimitedCalendarObjects((int)$calendar->getKey(), CalDavBackend::CALENDAR_TYPE_CALENDAR, ['uid', 'size']);
|
||||
foreach ($objects as $object) {
|
||||
$totalSize += (int)($object['size'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 150B for meta file per calendar + total calendar data size
|
||||
$size = ($calendarCount * 150 + $totalSize) / 1024;
|
||||
|
||||
return ceil($size);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
#[\Override]
|
||||
public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
|
||||
$output->writeln('Exporting calendaring data…');
|
||||
$this->exportVersion($exportDestination, $output);
|
||||
$this->exportCalendars($user, $exportDestination, $output);
|
||||
$this->exportSubscriptions($user, $exportDestination, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
private function exportVersion(IExportDestination $exportDestination, OutputInterface $output): void {
|
||||
try {
|
||||
$versionData = [
|
||||
'appVersion' => $this->appManager->getAppVersion(Application::APP_ID),
|
||||
];
|
||||
$exportDestination->addFileContents(self::PATH_VERSION, json_encode($versionData, JSON_THROW_ON_ERROR));
|
||||
} catch (Throwable $e) {
|
||||
throw new CalendarMigratorException('Could not export version information', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
public function exportCalendars(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
|
||||
$output->writeln('Exporting calendars to ' . self::PATH_CALENDARS . '…');
|
||||
|
||||
try {
|
||||
$calendarExports = $this->calendarManager->getCalendarsForPrincipal(self::USERS_URI_ROOT . $user->getUID());
|
||||
|
||||
$exportData = [];
|
||||
/** @var CalendarImpl $calendar */
|
||||
foreach ($calendarExports as $calendar) {
|
||||
$output->writeln('Exporting calendar "' . $calendar->getUri() . '"');
|
||||
|
||||
if (!$calendar instanceof CalendarImpl) {
|
||||
$output->writeln('Skipping unsupported calendar type for "' . $calendar->getUri() . '"');
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($calendar->isShared()) {
|
||||
$output->writeln('Skipping shared calendar "' . $calendar->getUri() . '"');
|
||||
continue;
|
||||
}
|
||||
|
||||
// construct archive path for calendar data
|
||||
$filename = preg_replace('/[^a-z0-9-_]/iu', '', $calendar->getUri());
|
||||
$exportDataPath = self::PATH_ROOT . $filename . '.data';
|
||||
|
||||
// add calendar metadata to the collection
|
||||
$exportData[] = [
|
||||
'format' => 'ical',
|
||||
'uri' => $calendar->getUri(),
|
||||
'label' => $calendar->getDisplayName(),
|
||||
'color' => $calendar->getDisplayColor(),
|
||||
'timezone' => $calendar->getSchedulingTimezone(),
|
||||
];
|
||||
|
||||
// export calendar data to a temporary file
|
||||
$options = new CalendarExportOptions();
|
||||
$options->setFormat('ical');
|
||||
$tempPath = $this->tempManager->getTemporaryFile();
|
||||
$tempFile = fopen($tempPath, 'w+');
|
||||
foreach ($this->exportService->export($calendar, $options) as $chunk) {
|
||||
fwrite($tempFile, $chunk);
|
||||
}
|
||||
|
||||
// add the temporary file to the export archive
|
||||
rewind($tempFile);
|
||||
$exportDestination->addFileAsStream($exportDataPath, $tempFile);
|
||||
fclose($tempFile);
|
||||
}
|
||||
|
||||
// write all calendar metadata
|
||||
$exportDestination->addFileContents(self::PATH_CALENDARS, json_encode($exportData, JSON_THROW_ON_ERROR));
|
||||
|
||||
$output->writeln('Exported ' . count($exportData) . ' calendar(s)…');
|
||||
} catch (Throwable $e) {
|
||||
throw new CalendarMigratorException('Could not export calendars', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
private function exportSubscriptions(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
|
||||
$output->writeln('Exporting calendar subscriptions to ' . self::PATH_SUBSCRIPTIONS . '…');
|
||||
|
||||
try {
|
||||
$subscriptions = $this->calDavBackend->getSubscriptionsForUser(self::USERS_URI_ROOT . $user->getUID());
|
||||
|
||||
$exportData = [];
|
||||
foreach ($subscriptions as $subscription) {
|
||||
$exportData[] = [
|
||||
'uri' => $subscription[self::DAV_PROPERTY_URI],
|
||||
'displayname' => $subscription[self::DAV_PROPERTY_DISPLAYNAME] ?? null,
|
||||
'color' => $subscription[self::DAV_PROPERTY_CALENDAR_COLOR] ?? null,
|
||||
'source' => $subscription[self::DAV_PROPERTY_SUBSCRIBED_SOURCE] ?? null,
|
||||
'striptodos' => $subscription[self::DAV_PROPERTY_SUBSCRIBED_STRIP_TODOS] ?? null,
|
||||
'stripalarms' => $subscription[self::DAV_PROPERTY_SUBSCRIBED_STRIP_ALARMS] ?? null,
|
||||
'stripattachments' => $subscription[self::DAV_PROPERTY_SUBSCRIBED_STRIP_ATTACHMENTS] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
$exportDestination->addFileContents(self::PATH_SUBSCRIPTIONS, json_encode($exportData, JSON_THROW_ON_ERROR));
|
||||
|
||||
$output->writeln('Exported ' . count($exportData) . ' calendar subscription(s)…');
|
||||
} catch (Throwable $e) {
|
||||
throw new CalendarMigratorException('Could not export calendar subscriptions', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
public function import(IUser $user, IImportSource $importSource, OutputInterface $output): void {
|
||||
$output->writeln('Importing calendaring data…');
|
||||
if ($importSource->getMigratorVersion($this->getId()) === null) {
|
||||
$output->writeln('No version for ' . static::class . ', skipping import…');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->importCalendars($user, $importSource, $output);
|
||||
$this->importSubscriptions($user, $importSource, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
public function importCalendars(IUser $user, IImportSource $importSource, OutputInterface $output): void {
|
||||
$output->writeln('Importing calendars from ' . self::PATH_ROOT . '…');
|
||||
|
||||
$migratorVersion = $importSource->getMigratorVersion($this->getId());
|
||||
match ($migratorVersion) {
|
||||
1 => $this->importCalendarsV1($user, $importSource, $output),
|
||||
2 => $this->importCalendarsV2($user, $importSource, $output),
|
||||
default => throw new CalendarMigratorException('Unsupported migrator version ' . $migratorVersion . ' for ' . static::class),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
public function importCalendarsV2(IUser $user, IImportSource $importSource, OutputInterface $output): void {
|
||||
$output->writeln('Importing calendars from ' . self::PATH_CALENDARS . '…');
|
||||
|
||||
if ($importSource->pathExists(self::PATH_CALENDARS) === false) {
|
||||
$output->writeln('No calendars to import…');
|
||||
return;
|
||||
}
|
||||
|
||||
$importData = $importSource->getFileContents(self::PATH_CALENDARS);
|
||||
if (empty($importData)) {
|
||||
$output->writeln('No calendars to import…');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var array<int, array<string, mixed>> $calendarsData */
|
||||
$calendarsData = json_decode($importData, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if (empty($calendarsData)) {
|
||||
$output->writeln('No calendars to import…');
|
||||
return;
|
||||
}
|
||||
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
|
||||
$importCount = 0;
|
||||
foreach ($calendarsData as $calendarMeta) {
|
||||
$migratedCalendarUri = self::MIGRATED_URI_PREFIX . $calendarMeta['uri'];
|
||||
$filename = preg_replace('/[^a-z0-9-_]/iu', '', $calendarMeta['uri']);
|
||||
$importDataPath = self::PATH_ROOT . $filename . '.data';
|
||||
|
||||
try {
|
||||
// check if a calendar with this URI already exists
|
||||
$calendars = $this->calendarManager->getCalendarsForPrincipal($principalUri, [$migratedCalendarUri]);
|
||||
if (empty($calendars)) {
|
||||
$output->writeln("Creating calendar \"$migratedCalendarUri\"");
|
||||
// create the calendar
|
||||
$this->calDavBackend->createCalendar($principalUri, $migratedCalendarUri, [
|
||||
self::DAV_PROPERTY_DISPLAYNAME => $calendarMeta['label'] ?? $this->l10n->t('Migrated calendar (%1$s)', [$calendarMeta['uri']]),
|
||||
self::DAV_PROPERTY_CALENDAR_COLOR => $calendarMeta['color'] ?? $this->defaults->getColorPrimary(),
|
||||
self::DAV_PROPERTY_CALENDAR_TIMEZONE => $calendarMeta['timezone'] ?? null,
|
||||
]);
|
||||
// retrieve the created calendar
|
||||
$calendars = $this->calendarManager->getCalendarsForPrincipal($principalUri, [$migratedCalendarUri]);
|
||||
if (empty($calendars) || !($calendars[0] instanceof CalendarImpl)) {
|
||||
$output->writeln("Failed to retrieve created calendar \"$migratedCalendarUri\", skipping import…");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$output->writeln("Using existing calendar \"$migratedCalendarUri\"");
|
||||
}
|
||||
$calendar = $calendars[0];
|
||||
|
||||
// copy import stream to temporary file as the source stream is not rewindable
|
||||
$importStream = $importSource->getFileAsStream($importDataPath);
|
||||
$tempPath = $this->tempManager->getTemporaryFile();
|
||||
$tempFile = fopen($tempPath, 'w+');
|
||||
stream_copy_to_stream($importStream, $tempFile);
|
||||
rewind($tempFile);
|
||||
|
||||
// import calendar data
|
||||
try {
|
||||
$options = new CalendarImportOptions();
|
||||
$options->setFormat($calendarMeta['format'] ?? 'ical');
|
||||
$options->setErrors(0);
|
||||
$options->setValidate(1);
|
||||
$options->setSupersede(true);
|
||||
|
||||
$outcome = $this->importService->import(
|
||||
$tempFile,
|
||||
$calendar,
|
||||
$options
|
||||
);
|
||||
} finally {
|
||||
fclose($tempFile);
|
||||
}
|
||||
|
||||
$this->importSummary($calendarMeta['label'] ?? $calendarMeta['uri'], $outcome, $output);
|
||||
|
||||
$importCount++;
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln('Failed to import calendar "' . ($calendarMeta['uri'] ?? 'unknown') . '", skipping…');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('Imported ' . $importCount . ' calendar(s)…');
|
||||
} catch (Throwable $e) {
|
||||
throw new CalendarMigratorException('Could not import calendars', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
public function importCalendarsV1(IUser $user, IImportSource $importSource, OutputInterface $output): void {
|
||||
$files = $importSource->getFolderListing(self::PATH_ROOT);
|
||||
if (empty($files)) {
|
||||
$output->writeln('No calendars to import…');
|
||||
}
|
||||
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
|
||||
foreach ($files as $filename) {
|
||||
// Only process .ics files
|
||||
if (!str_ends_with($filename, '.ics')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// construct archive path
|
||||
$importDataPath = self::PATH_ROOT . $filename;
|
||||
|
||||
try {
|
||||
$calendarUri = substr($filename, 0, -4);
|
||||
$migratedCalendarUri = self::MIGRATED_URI_PREFIX . $calendarUri;
|
||||
|
||||
// copy import stream to temporary file as the source stream is not rewindable
|
||||
$importStream = $importSource->getFileAsStream($importDataPath);
|
||||
$tempPath = $this->tempManager->getTemporaryFile();
|
||||
$tempFile = fopen($tempPath, 'w+');
|
||||
stream_copy_to_stream($importStream, $tempFile);
|
||||
rewind($tempFile);
|
||||
|
||||
// check if a calendar with this URI already exists
|
||||
$calendars = $this->calendarManager->getCalendarsForPrincipal($principalUri, [$migratedCalendarUri]);
|
||||
if (empty($calendars)) {
|
||||
$output->writeln("Creating calendar \"$migratedCalendarUri\"");
|
||||
// extract calendar properties from the ICS header without full parsing
|
||||
$calendarName = null;
|
||||
$calendarColor = null;
|
||||
$headerLines = 0;
|
||||
while (($line = fgets($tempFile)) !== false && $headerLines < 50) {
|
||||
$headerLines++;
|
||||
$line = trim($line);
|
||||
if (str_starts_with($line, 'X-WR-CALNAME:')) {
|
||||
$calendarName = substr($line, 13);
|
||||
} elseif (str_starts_with($line, 'X-APPLE-CALENDAR-COLOR:')) {
|
||||
$calendarColor = substr($line, 23);
|
||||
}
|
||||
// stop parsing header once we hit the first component
|
||||
if (str_starts_with($line, 'BEGIN:VEVENT')
|
||||
|| str_starts_with($line, 'BEGIN:VTODO')
|
||||
|| str_starts_with($line, 'BEGIN:VJOURNAL')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rewind($tempFile);
|
||||
// create the calendar
|
||||
$this->calDavBackend->createCalendar($principalUri, $migratedCalendarUri, [
|
||||
self::DAV_PROPERTY_DISPLAYNAME => $calendarName ?? $this->l10n->t('Migrated calendar (%1$s)', [$calendarUri]),
|
||||
self::DAV_PROPERTY_CALENDAR_COLOR => $calendarColor ?? $this->defaults->getColorPrimary(),
|
||||
]);
|
||||
// retrieve the created calendar
|
||||
$calendars = $this->calendarManager->getCalendarsForPrincipal($principalUri, [$migratedCalendarUri]);
|
||||
if (empty($calendars) || !($calendars[0] instanceof CalendarImpl)) {
|
||||
$output->writeln("Failed to retrieve created calendar \"$migratedCalendarUri\", skipping import…");
|
||||
fclose($tempFile);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$output->writeln("Using existing calendar \"$migratedCalendarUri\"");
|
||||
}
|
||||
$calendar = $calendars[0];
|
||||
|
||||
// import calendar data
|
||||
$options = new CalendarImportOptions();
|
||||
$options->setFormat('ical');
|
||||
$options->setErrors(0);
|
||||
$options->setValidate(1);
|
||||
$options->setSupersede(true);
|
||||
|
||||
try {
|
||||
$outcome = $this->importService->import(
|
||||
$tempFile,
|
||||
$calendar,
|
||||
$options
|
||||
);
|
||||
} finally {
|
||||
fclose($tempFile);
|
||||
}
|
||||
|
||||
$this->importSummary($calendarName ?? $calendarUri, $outcome, $output);
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln("Failed to import calendar \"$filename\", skipping…");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CalendarMigratorException
|
||||
*/
|
||||
public function importSubscriptions(IUser $user, IImportSource $importSource, OutputInterface $output): void {
|
||||
$output->writeln('Importing calendar subscriptions from ' . self::PATH_SUBSCRIPTIONS . '…');
|
||||
|
||||
if ($importSource->pathExists(self::PATH_SUBSCRIPTIONS) === false) {
|
||||
$output->writeln('No calendar subscriptions to import…');
|
||||
return;
|
||||
}
|
||||
|
||||
$importData = $importSource->getFileContents(self::PATH_SUBSCRIPTIONS);
|
||||
if (empty($importData)) {
|
||||
$output->writeln('No calendar subscriptions to import…');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$subscriptions = json_decode($importData, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if (empty($subscriptions)) {
|
||||
$output->writeln('No calendar subscriptions to import…');
|
||||
return;
|
||||
}
|
||||
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
$importCount = 0;
|
||||
foreach ($subscriptions as $subscription) {
|
||||
$output->writeln('Importing calendar subscription "' . ($subscription['displayname'] ?? $subscription['source'] ?? 'unknown') . '"');
|
||||
|
||||
if (empty($subscription['source'])) {
|
||||
$output->writeln('Skipping subscription without source URL');
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->calDavBackend->createSubscription(
|
||||
$principalUri,
|
||||
$subscription['uri'] ? self::MIGRATED_URI_PREFIX . $subscription['uri'] : self::MIGRATED_URI_PREFIX . bin2hex(random_bytes(16)),
|
||||
[
|
||||
'{http://calendarserver.org/ns/}source' => new Href($subscription['source']),
|
||||
self::DAV_PROPERTY_DISPLAYNAME => $subscription['displayname'] ?? null,
|
||||
self::DAV_PROPERTY_CALENDAR_COLOR => $subscription['color'] ?? null,
|
||||
self::DAV_PROPERTY_SUBSCRIBED_STRIP_TODOS => $subscription['striptodos'] ?? null,
|
||||
self::DAV_PROPERTY_SUBSCRIBED_STRIP_ALARMS => $subscription['stripalarms'] ?? null,
|
||||
self::DAV_PROPERTY_SUBSCRIBED_STRIP_ATTACHMENTS => $subscription['stripattachments'] ?? null,
|
||||
]
|
||||
);
|
||||
$importCount++;
|
||||
}
|
||||
|
||||
$output->writeln('Imported ' . $importCount . ' subscription(s)…');
|
||||
} catch (Throwable $e) {
|
||||
throw new CalendarMigratorException('Could not import calendar subscriptions', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function importSummary(string $label, array $outcome, OutputInterface $output): void {
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ($outcome as $result) {
|
||||
match ($result['outcome'] ?? null) {
|
||||
'created' => $created++,
|
||||
'updated' => $updated++,
|
||||
'exists' => $skipped++,
|
||||
'error' => $errors++,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
$output->writeln(" \"$label\": $created created, $updated updated, $skipped skipped, $errors errors");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -10,107 +10,629 @@ declare(strict_types=1);
|
||||
namespace OCA\DAV\Tests\integration\UserMigration;
|
||||
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\CalendarImpl;
|
||||
use OCA\DAV\UserMigration\CalendarMigrator;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\Calendar\IManager as ICalendarManager;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use Sabre\VObject\Component as VObjectComponent;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Property as VObjectProperty;
|
||||
use Sabre\VObject\Reader as VObjectReader;
|
||||
use OCP\UserMigration\IExportDestination;
|
||||
use OCP\UserMigration\IImportSource;
|
||||
use Sabre\VObject\UUIDUtil;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Test\TestCase;
|
||||
use function scandir;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\Group(name: 'DB')]
|
||||
class CalendarMigratorTest extends TestCase {
|
||||
|
||||
private IUserManager $userManager;
|
||||
|
||||
private ICalendarManager $calendarManager;
|
||||
private CalDavBackend $calDavBackend;
|
||||
private CalendarMigrator $migrator;
|
||||
|
||||
private OutputInterface $output;
|
||||
|
||||
private const ASSETS_DIR = __DIR__ . '/assets/calendars/';
|
||||
private const USERS_URI_ROOT = 'principals/users/';
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$app = new App(Application::APP_ID);
|
||||
$container = $app->getContainer();
|
||||
|
||||
$this->userManager = $container->get(IUserManager::class);
|
||||
$this->calendarManager = $container->get(ICalendarManager::class);
|
||||
$this->calDavBackend = $container->get(CalDavBackend::class);
|
||||
$this->migrator = $container->get(CalendarMigrator::class);
|
||||
$this->output = $this->createMock(OutputInterface::class);
|
||||
}
|
||||
|
||||
public static function dataAssets(): array {
|
||||
return array_map(
|
||||
function (string $filename) {
|
||||
/** @var VCalendar $vCalendar */
|
||||
$vCalendar = VObjectReader::read(
|
||||
fopen(self::ASSETS_DIR . $filename, 'r'),
|
||||
VObjectReader::OPTION_FORGIVING,
|
||||
);
|
||||
[$initialCalendarUri, $ext] = explode('.', $filename, 2);
|
||||
return [UUIDUtil::getUUID(), $filename, $initialCalendarUri, $vCalendar];
|
||||
},
|
||||
array_diff(
|
||||
scandir(self::ASSETS_DIR),
|
||||
// Exclude current and parent directories
|
||||
['.', '..'],
|
||||
),
|
||||
);
|
||||
private function createTestUser(): IUser {
|
||||
$userId = UUIDUtil::getUUID();
|
||||
return $this->userManager->createUser($userId, 'topsecretpassword');
|
||||
}
|
||||
|
||||
private function getProperties(VCalendar $vCalendar): array {
|
||||
return array_map(
|
||||
fn (VObjectProperty $property) => $property->serialize(),
|
||||
array_values(array_filter(
|
||||
$vCalendar->children(),
|
||||
fn ($child) => $child instanceof VObjectProperty,
|
||||
)),
|
||||
);
|
||||
private function deleteUser(IUser $user): void {
|
||||
$user->delete();
|
||||
}
|
||||
|
||||
private function getComponents(VCalendar $vCalendar): array {
|
||||
return array_map(
|
||||
// Elements of the serialized blob are sorted
|
||||
fn (VObjectComponent $component) => $component->serialize(),
|
||||
$vCalendar->getComponents(),
|
||||
);
|
||||
private function getCalendarsForUser(IUser $user): array {
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
$calendars = $this->calendarManager->getCalendarsForPrincipal($principalUri);
|
||||
return array_filter($calendars, fn ($c) => $c instanceof CalendarImpl && !$c->isShared());
|
||||
}
|
||||
|
||||
private function getSanitizedComponents(VCalendar $vCalendar): array {
|
||||
return array_map(
|
||||
// Elements of the serialized blob are sorted
|
||||
fn (VObjectComponent $component) => $this->invokePrivate($this->migrator, 'sanitizeComponent', [$component])->serialize(),
|
||||
$vCalendar->getComponents(),
|
||||
);
|
||||
public function testImportV1(): void {
|
||||
$user = $this->createTestUser();
|
||||
|
||||
try {
|
||||
// Get all asset files
|
||||
$files = scandir(self::ASSETS_DIR);
|
||||
$this->assertNotFalse($files, 'Failed to scan assets directory');
|
||||
$files = array_values(array_diff($files, ['.', '..']));
|
||||
$this->assertNotEmpty($files, 'No asset files found');
|
||||
|
||||
// Load all ICS content
|
||||
$icsContents = [];
|
||||
foreach ($files as $filename) {
|
||||
$icsContents[$filename] = file_get_contents(self::ASSETS_DIR . $filename);
|
||||
}
|
||||
|
||||
// Setup import source mock
|
||||
$importSource = $this->createMock(IImportSource::class);
|
||||
$importSource->method('getMigratorVersion')
|
||||
->with('calendar')
|
||||
->willReturn(1);
|
||||
$importSource->method('getFolderListing')
|
||||
->with('dav/calendars/')
|
||||
->willReturn($files);
|
||||
$importSource->method('getFileAsStream')
|
||||
->willReturnCallback(function (string $path) use ($icsContents) {
|
||||
foreach ($icsContents as $filename => $content) {
|
||||
if ($path === 'dav/calendars/' . $filename) {
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $content);
|
||||
rewind($stream);
|
||||
return $stream;
|
||||
}
|
||||
}
|
||||
throw new \Exception("Unexpected path: $path");
|
||||
});
|
||||
|
||||
// Import all calendars
|
||||
$this->migrator->import($user, $importSource, $this->output);
|
||||
|
||||
// Verify all calendars were created
|
||||
$calendars = $this->getCalendarsForUser($user);
|
||||
$this->assertCount(count($files), $calendars, 'Expected all calendars to be created');
|
||||
|
||||
// Verify each calendar has the migrated prefix and has objects
|
||||
foreach ($files as $filename) {
|
||||
$expectedUri = 'migrated-' . substr($filename, 0, -4);
|
||||
$found = false;
|
||||
foreach ($calendars as $calendar) {
|
||||
if ($calendar->getUri() === $expectedUri) {
|
||||
$found = true;
|
||||
// Verify calendar has objects
|
||||
$objects = $this->calDavBackend->getCalendarObjects((int)$calendar->getKey());
|
||||
$this->assertNotEmpty($objects, "Expected calendar $expectedUri to have objects");
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertTrue($found, "Calendar with URI $expectedUri was not found");
|
||||
}
|
||||
} finally {
|
||||
$this->deleteUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataAssets')]
|
||||
public function testImportExportAsset(string $userId, string $filename, string $initialCalendarUri, VCalendar $importCalendar): void {
|
||||
$user = $this->userManager->createUser($userId, 'topsecretpassword');
|
||||
public function testImportV2(): void {
|
||||
$user = $this->createTestUser();
|
||||
|
||||
$problems = $importCalendar->validate();
|
||||
$this->assertEmpty($problems);
|
||||
try {
|
||||
// Get all asset files
|
||||
$files = scandir(self::ASSETS_DIR);
|
||||
$this->assertNotFalse($files, 'Failed to scan assets directory');
|
||||
$files = array_values(array_diff($files, ['.', '..']));
|
||||
$this->assertNotEmpty($files, 'No asset files found');
|
||||
|
||||
$this->invokePrivate($this->migrator, 'importCalendar', [$user, $filename, $initialCalendarUri, $importCalendar, $this->output]);
|
||||
// Load all ICS content and build calendars metadata
|
||||
$calendarsMetadata = [];
|
||||
$icsContents = [];
|
||||
foreach ($files as $filename) {
|
||||
$icsContent = file_get_contents(self::ASSETS_DIR . $filename);
|
||||
$calendarUri = substr($filename, 0, -4);
|
||||
$icsContents[$calendarUri] = $icsContent;
|
||||
|
||||
$calendarExports = $this->invokePrivate($this->migrator, 'getCalendarExports', [$user, $this->output]);
|
||||
$this->assertCount(1, $calendarExports);
|
||||
$calendarsMetadata[] = [
|
||||
'format' => 'ical',
|
||||
'uri' => $calendarUri,
|
||||
'label' => $calendarUri,
|
||||
'color' => '#0082c9',
|
||||
'timezone' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/** @var VCalendar $exportCalendar */
|
||||
['vCalendar' => $exportCalendar] = reset($calendarExports);
|
||||
// Setup import source mock for V2 format (calendars.json + .data files)
|
||||
$importSource = $this->createMock(IImportSource::class);
|
||||
$calendarsJson = json_encode($calendarsMetadata);
|
||||
|
||||
$this->assertEqualsCanonicalizing(
|
||||
$this->getProperties($importCalendar),
|
||||
$this->getProperties($exportCalendar),
|
||||
);
|
||||
$importSource->method('getMigratorVersion')
|
||||
->with('calendar')
|
||||
->willReturn(2);
|
||||
|
||||
$this->assertEqualsCanonicalizing(
|
||||
// Components are expected to be sanitized on import
|
||||
$this->getSanitizedComponents($importCalendar),
|
||||
$this->getComponents($exportCalendar),
|
||||
);
|
||||
$importSource->method('pathExists')
|
||||
->willReturnCallback(fn (string $path) => $path === 'dav/calendars/calendars.json');
|
||||
|
||||
$importSource->method('getFileContents')
|
||||
->willReturnCallback(function (string $path) use ($calendarsJson) {
|
||||
if ($path === 'dav/calendars/calendars.json') {
|
||||
return $calendarsJson;
|
||||
}
|
||||
throw new \Exception("Unexpected path: $path");
|
||||
});
|
||||
|
||||
$importSource->method('getFileAsStream')
|
||||
->willReturnCallback(function (string $path) use ($icsContents) {
|
||||
foreach ($icsContents as $calendarUri => $icsContent) {
|
||||
if ($path === 'dav/calendars/' . $calendarUri . '.data') {
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $icsContent);
|
||||
rewind($stream);
|
||||
return $stream;
|
||||
}
|
||||
}
|
||||
throw new \Exception("Unexpected path: $path");
|
||||
});
|
||||
|
||||
// Import all calendars
|
||||
$this->migrator->import($user, $importSource, $this->output);
|
||||
|
||||
// Verify all calendars were created
|
||||
$calendars = $this->getCalendarsForUser($user);
|
||||
$this->assertCount(count($files), $calendars, 'Expected all calendars to be created');
|
||||
|
||||
// Verify each calendar has the correct properties and objects
|
||||
foreach ($calendarsMetadata as $metadata) {
|
||||
$expectedUri = 'migrated-' . $metadata['uri'];
|
||||
$found = false;
|
||||
foreach ($calendars as $calendar) {
|
||||
if ($calendar->getUri() === $expectedUri) {
|
||||
$found = true;
|
||||
// Verify calendar display name
|
||||
$this->assertEquals($metadata['label'], $calendar->getDisplayName());
|
||||
// Verify calendar has objects
|
||||
$objects = $this->calDavBackend->getCalendarObjects((int)$calendar->getKey());
|
||||
$this->assertNotEmpty($objects, "Expected calendar $expectedUri to have objects");
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertTrue($found, "Calendar with URI $expectedUri was not found");
|
||||
}
|
||||
} finally {
|
||||
$this->deleteUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
public function testExport(): void {
|
||||
$user = $this->createTestUser();
|
||||
|
||||
try {
|
||||
// Create a calendar to export
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
$calendarUri = 'test-export-calendar';
|
||||
$calendarId = $this->calDavBackend->createCalendar($principalUri, $calendarUri, [
|
||||
'{DAV:}displayname' => 'Test Export Calendar',
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#ff0000',
|
||||
]);
|
||||
|
||||
// Add an event to the calendar
|
||||
$icsContent = file_get_contents(self::ASSETS_DIR . 'event-timed.ics');
|
||||
$this->calDavBackend->createCalendarObject($calendarId, 'test-event.ics', $icsContent);
|
||||
|
||||
// Setup export destination mock
|
||||
$exportDestination = $this->createMock(IExportDestination::class);
|
||||
|
||||
$exportedCalendarsJson = null;
|
||||
$exportedData = null;
|
||||
|
||||
$exportDestination->method('addFileContents')
|
||||
->willReturnCallback(function (string $path, string $content) use (&$exportedCalendarsJson) {
|
||||
if ($path === 'dav/calendars/calendars.json') {
|
||||
$exportedCalendarsJson = json_decode($content, true);
|
||||
}
|
||||
});
|
||||
|
||||
$exportDestination->method('addFileAsStream')
|
||||
->willReturnCallback(function (string $path, $stream) use (&$exportedData) {
|
||||
if (str_ends_with($path, '.data')) {
|
||||
$exportedData = stream_get_contents($stream);
|
||||
}
|
||||
});
|
||||
|
||||
// Export the calendar
|
||||
$this->migrator->export($user, $exportDestination, $this->output);
|
||||
|
||||
// Verify calendars.json was exported
|
||||
$this->assertNotNull($exportedCalendarsJson, 'Expected calendars.json to be exported');
|
||||
$this->assertIsArray($exportedCalendarsJson);
|
||||
$this->assertCount(1, $exportedCalendarsJson);
|
||||
$exportedMeta = $exportedCalendarsJson[0];
|
||||
$this->assertEquals('ical', $exportedMeta['format']);
|
||||
$this->assertEquals($calendarUri, $exportedMeta['uri']);
|
||||
$this->assertEquals('Test Export Calendar', $exportedMeta['label']);
|
||||
$this->assertEquals('#ff0000', $exportedMeta['color']);
|
||||
|
||||
// Verify data was exported
|
||||
$this->assertNotNull($exportedData, 'Expected data to be exported');
|
||||
$this->assertIsString($exportedData);
|
||||
/** @var string $exportedData */
|
||||
$this->assertStringContainsString('BEGIN:VCALENDAR', $exportedData);
|
||||
$this->assertStringContainsString('BEGIN:VEVENT', $exportedData);
|
||||
} finally {
|
||||
$this->deleteUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
public function testExportImportRoundTrip(): void {
|
||||
$user = $this->createTestUser();
|
||||
|
||||
try {
|
||||
// Create a calendar with some events
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
$calendarUri = 'roundtrip-calendar';
|
||||
$calendarId = $this->calDavBackend->createCalendar($principalUri, $calendarUri, [
|
||||
'{DAV:}displayname' => 'Round Trip Calendar',
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#00ff00',
|
||||
]);
|
||||
|
||||
// Add events to the calendar
|
||||
$icsContent = file_get_contents(self::ASSETS_DIR . 'event-timed.ics');
|
||||
$this->calDavBackend->createCalendarObject($calendarId, 'event1.ics', $icsContent);
|
||||
|
||||
// Capture exported data
|
||||
$exportedFiles = [];
|
||||
|
||||
$exportDestination = $this->createMock(IExportDestination::class);
|
||||
$exportDestination->method('addFileContents')
|
||||
->willReturnCallback(function (string $path, string $content) use (&$exportedFiles) {
|
||||
$exportedFiles[$path] = $content;
|
||||
});
|
||||
$exportDestination->method('addFileAsStream')
|
||||
->willReturnCallback(function (string $path, $stream) use (&$exportedFiles) {
|
||||
$exportedFiles[$path] = stream_get_contents($stream);
|
||||
});
|
||||
|
||||
// Export
|
||||
$this->migrator->export($user, $exportDestination, $this->output);
|
||||
|
||||
// Delete the original calendar
|
||||
$this->calDavBackend->deleteCalendar($calendarId, true);
|
||||
|
||||
// Verify calendar is gone
|
||||
$calendars = $this->getCalendarsForUser($user);
|
||||
$this->assertEmpty($calendars, 'Calendar should be deleted');
|
||||
|
||||
// Setup import source from exported data
|
||||
$importSource = $this->createMock(IImportSource::class);
|
||||
$importSource->method('getMigratorVersion')
|
||||
->with('calendar')
|
||||
->willReturn(2);
|
||||
|
||||
$importSource->method('pathExists')
|
||||
->willReturnCallback(function (string $path) use ($exportedFiles) {
|
||||
return isset($exportedFiles[$path]);
|
||||
});
|
||||
$importSource->method('getFolderListing')
|
||||
->with('dav/calendars/')
|
||||
->willReturn(array_map(fn ($p) => basename($p), array_keys($exportedFiles)));
|
||||
|
||||
$importSource->method('getFileContents')
|
||||
->willReturnCallback(function (string $path) use ($exportedFiles) {
|
||||
if (isset($exportedFiles[$path])) {
|
||||
return $exportedFiles[$path];
|
||||
}
|
||||
throw new \Exception("File not found: $path");
|
||||
});
|
||||
|
||||
$importSource->method('getFileAsStream')
|
||||
->willReturnCallback(function (string $path) use ($exportedFiles) {
|
||||
if (isset($exportedFiles[$path])) {
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $exportedFiles[$path]);
|
||||
rewind($stream);
|
||||
return $stream;
|
||||
}
|
||||
throw new \Exception("File not found: $path");
|
||||
});
|
||||
|
||||
// Import
|
||||
$this->migrator->import($user, $importSource, $this->output);
|
||||
|
||||
// Verify calendar was recreated with migrated prefix
|
||||
$calendars = $this->getCalendarsForUser($user);
|
||||
$this->assertCount(1, $calendars, 'Expected one calendar after import');
|
||||
|
||||
$calendar = reset($calendars);
|
||||
$this->assertEquals('migrated-' . $calendarUri, $calendar->getUri());
|
||||
$this->assertEquals('Round Trip Calendar', $calendar->getDisplayName());
|
||||
|
||||
// Verify events were imported
|
||||
$objects = $this->calDavBackend->getCalendarObjects((int)$calendar->getKey());
|
||||
$this->assertCount(1, $objects, 'Expected one event after import');
|
||||
} finally {
|
||||
$this->deleteUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetEstimatedExportSize(): void {
|
||||
$user = $this->createTestUser();
|
||||
|
||||
try {
|
||||
// Initially should be 0 or minimal
|
||||
$initialSize = $this->migrator->getEstimatedExportSize($user);
|
||||
$this->assertEquals(0, $initialSize);
|
||||
|
||||
// Create a calendar with events
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
$calendarUri = 'size-test-calendar';
|
||||
$calendarId = $this->calDavBackend->createCalendar($principalUri, $calendarUri, [
|
||||
'{DAV:}displayname' => 'Size Test Calendar',
|
||||
]);
|
||||
|
||||
// Add an event
|
||||
$icsContent = file_get_contents(self::ASSETS_DIR . 'event-timed.ics');
|
||||
$this->calDavBackend->createCalendarObject($calendarId, 'event.ics', $icsContent);
|
||||
|
||||
// Size should now be > 0
|
||||
$sizeWithData = $this->migrator->getEstimatedExportSize($user);
|
||||
$this->assertGreaterThan(0, $sizeWithData);
|
||||
} finally {
|
||||
$this->deleteUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
public function testImportExistingCalendarSkipped(): void {
|
||||
$user = $this->createTestUser();
|
||||
|
||||
try {
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
|
||||
// Pre-create a calendar with the migrated prefix
|
||||
$calendarUri = 'migrated-existing-calendar';
|
||||
$this->calDavBackend->createCalendar($principalUri, $calendarUri, [
|
||||
'{DAV:}displayname' => 'Existing Calendar',
|
||||
]);
|
||||
|
||||
// Setup import for V2
|
||||
$importSource = $this->createMock(IImportSource::class);
|
||||
$importSource->method('getMigratorVersion')
|
||||
->with('calendar')
|
||||
->willReturn(2);
|
||||
|
||||
$importSource->method('pathExists')
|
||||
->willReturnCallback(function (string $path) {
|
||||
if ($path === 'dav/calendars/calendars.json') {
|
||||
return true;
|
||||
}
|
||||
if ($path === 'dav/calendars/subscriptions.json') {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
$importSource->method('getFileContents')
|
||||
->willReturnCallback(function (string $path) {
|
||||
if ($path === 'dav/calendars/calendars.json') {
|
||||
return json_encode([[
|
||||
'format' => 'ical',
|
||||
'uri' => 'existing-calendar',
|
||||
'label' => 'Existing Calendar',
|
||||
'color' => '#0082c9',
|
||||
'timezone' => null,
|
||||
]]);
|
||||
}
|
||||
throw new \Exception("Unexpected path: $path");
|
||||
});
|
||||
// Import should use existing calendar
|
||||
$this->migrator->import($user, $importSource, $this->output);
|
||||
|
||||
// Should still have just one calendar
|
||||
$calendars = $this->getCalendarsForUser($user);
|
||||
$this->assertCount(1, $calendars);
|
||||
} finally {
|
||||
$this->deleteUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
public function testExportSubscriptions(): void {
|
||||
$user = $this->createTestUser();
|
||||
|
||||
try {
|
||||
// Create a subscription to export
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
$this->calDavBackend->createSubscription(
|
||||
$principalUri,
|
||||
'test-subscription',
|
||||
[
|
||||
'{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('https://example.com/calendar.ics'),
|
||||
'{DAV:}displayname' => 'Test Subscription',
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#ff0000',
|
||||
'{http://calendarserver.org/ns/}subscribed-strip-todos' => '1',
|
||||
]
|
||||
);
|
||||
|
||||
// Setup export destination mock
|
||||
$exportDestination = $this->createMock(IExportDestination::class);
|
||||
|
||||
$exportedSubscriptionsJson = null;
|
||||
|
||||
$exportDestination->method('addFileContents')
|
||||
->willReturnCallback(function (string $path, string $content) use (&$exportedSubscriptionsJson) {
|
||||
if ($path === 'dav/calendars/subscriptions.json') {
|
||||
$exportedSubscriptionsJson = json_decode($content, true);
|
||||
}
|
||||
});
|
||||
|
||||
$exportDestination->method('addFileAsStream');
|
||||
|
||||
// Export
|
||||
$this->migrator->export($user, $exportDestination, $this->output);
|
||||
|
||||
// Verify exported subscription data
|
||||
$this->assertNotNull($exportedSubscriptionsJson, 'Subscriptions JSON should be exported');
|
||||
$this->assertCount(1, $exportedSubscriptionsJson, 'Expected one subscription in export');
|
||||
|
||||
$exportedSubscription = $exportedSubscriptionsJson[0];
|
||||
$this->assertEquals('test-subscription', $exportedSubscription['uri']);
|
||||
$this->assertEquals('Test Subscription', $exportedSubscription['displayname']);
|
||||
$this->assertEquals('#ff0000', $exportedSubscription['color']);
|
||||
$this->assertEquals('https://example.com/calendar.ics', $exportedSubscription['source']);
|
||||
$this->assertEquals('1', $exportedSubscription['striptodos']);
|
||||
} finally {
|
||||
$this->deleteUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
public function testImportSubscriptions(): void {
|
||||
$user = $this->createTestUser();
|
||||
|
||||
try {
|
||||
// Setup import source mock
|
||||
$importSource = $this->createMock(IImportSource::class);
|
||||
|
||||
$subscriptionsJson = json_encode([[
|
||||
'uri' => 'imported-subscription',
|
||||
'displayname' => 'Imported Subscription',
|
||||
'color' => '#00ff00',
|
||||
'source' => 'https://example.com/imported.ics',
|
||||
'striptodos' => null,
|
||||
'stripalarms' => '1',
|
||||
'stripattachments' => null,
|
||||
]]);
|
||||
|
||||
$importSource->method('getMigratorVersion')
|
||||
->with('calendar')
|
||||
->willReturn(2);
|
||||
|
||||
$importSource->method('pathExists')
|
||||
->willReturnCallback(function (string $path) {
|
||||
if ($path === 'dav/calendars/subscriptions.json') {
|
||||
return true;
|
||||
}
|
||||
if ($path === 'dav/calendars/calendars.json') {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
$importSource->method('getFileContents')
|
||||
->willReturnCallback(function (string $path) use ($subscriptionsJson) {
|
||||
if ($path === 'dav/calendars/subscriptions.json') {
|
||||
return $subscriptionsJson;
|
||||
}
|
||||
if ($path === 'dav/calendars/calendars.json') {
|
||||
// Return empty calendars array
|
||||
return json_encode([]);
|
||||
}
|
||||
throw new \Exception("Unexpected path: $path");
|
||||
});
|
||||
|
||||
// Import
|
||||
$this->migrator->import($user, $importSource, $this->output);
|
||||
|
||||
// Verify subscription was created
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
$subscriptions = $this->calDavBackend->getSubscriptionsForUser($principalUri);
|
||||
$this->assertCount(1, $subscriptions);
|
||||
|
||||
$subscription = $subscriptions[0];
|
||||
$this->assertEquals('migrated-imported-subscription', $subscription['uri']);
|
||||
$this->assertEquals('Imported Subscription', $subscription['{DAV:}displayname']);
|
||||
$this->assertEquals('#00ff00', $subscription['{http://apple.com/ns/ical/}calendar-color']);
|
||||
$this->assertEquals('1', $subscription['{http://calendarserver.org/ns/}subscribed-strip-alarms']);
|
||||
} finally {
|
||||
$this->deleteUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
public function testExportImportSubscriptionsRoundTrip(): void {
|
||||
$user = $this->createTestUser();
|
||||
|
||||
try {
|
||||
// Create subscriptions to export
|
||||
$principalUri = self::USERS_URI_ROOT . $user->getUID();
|
||||
$this->calDavBackend->createSubscription(
|
||||
$principalUri,
|
||||
'roundtrip-subscription',
|
||||
[
|
||||
'{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('https://example.com/roundtrip.ics'),
|
||||
'{DAV:}displayname' => 'Round Trip Subscription',
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#0000ff',
|
||||
]
|
||||
);
|
||||
|
||||
// Capture exported data
|
||||
$exportedFiles = [];
|
||||
|
||||
$exportDestination = $this->createMock(IExportDestination::class);
|
||||
$exportDestination->method('addFileContents')
|
||||
->willReturnCallback(function (string $path, string $content) use (&$exportedFiles) {
|
||||
$exportedFiles[$path] = $content;
|
||||
});
|
||||
|
||||
$exportDestination->method('addFileAsStream');
|
||||
|
||||
// Export
|
||||
$this->migrator->export($user, $exportDestination, $this->output);
|
||||
|
||||
// Delete the original subscription
|
||||
$subscriptions = $this->calDavBackend->getSubscriptionsForUser($principalUri);
|
||||
foreach ($subscriptions as $subscription) {
|
||||
$this->calDavBackend->deleteSubscription($subscription['id']);
|
||||
}
|
||||
|
||||
// Verify subscription is gone
|
||||
$subscriptions = $this->calDavBackend->getSubscriptionsForUser($principalUri);
|
||||
$this->assertEmpty($subscriptions, 'Subscription should be deleted');
|
||||
|
||||
// Setup import source from exported data
|
||||
$importSource = $this->createMock(IImportSource::class);
|
||||
$importSource->method('getMigratorVersion')
|
||||
->with('calendar')
|
||||
->willReturn(2);
|
||||
|
||||
$importSource->method('pathExists')
|
||||
->willReturnCallback(function (string $path) use ($exportedFiles) {
|
||||
return isset($exportedFiles[$path]);
|
||||
});
|
||||
|
||||
$importSource->method('getFileContents')
|
||||
->willReturnCallback(function (string $path) use ($exportedFiles) {
|
||||
if (isset($exportedFiles[$path])) {
|
||||
return $exportedFiles[$path];
|
||||
}
|
||||
// Return empty for missing files
|
||||
if ($path === 'dav/calendars/calendars.json') {
|
||||
return json_encode([]);
|
||||
}
|
||||
throw new \Exception("File not found: $path");
|
||||
});
|
||||
|
||||
// Import
|
||||
$this->migrator->import($user, $importSource, $this->output);
|
||||
|
||||
// Verify subscription was recreated with migrated prefix
|
||||
$subscriptions = $this->calDavBackend->getSubscriptionsForUser($principalUri);
|
||||
$this->assertCount(1, $subscriptions, 'Expected one subscription after import');
|
||||
|
||||
$subscription = $subscriptions[0];
|
||||
$this->assertEquals('migrated-roundtrip-subscription', $subscription['uri']);
|
||||
$this->assertEquals('Round Trip Subscription', $subscription['{DAV:}displayname']);
|
||||
$this->assertEquals('#0000ff', $subscription['{http://apple.com/ns/ical/}calendar-color']);
|
||||
} finally {
|
||||
$this->deleteUser($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1881,4 +1881,43 @@ EOD;
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public function testDefaultAlarmProperty(): void {
|
||||
$calendarId = $this->createTestCalendar();
|
||||
|
||||
// Test setting default alarm property to 15 minutes before (-900 seconds)
|
||||
$patch = new PropPatch([
|
||||
'{http://nextcloud.com/ns}default-alarm' => -900
|
||||
]);
|
||||
$this->backend->updateCalendar($calendarId, $patch);
|
||||
$patch->commit();
|
||||
|
||||
// Verify the property was set
|
||||
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
|
||||
$this->assertCount(1, $calendars);
|
||||
$this->assertEquals(-900, $calendars[0]['{http://nextcloud.com/ns}default-alarm']);
|
||||
|
||||
// Test updating to a different value (1 day before = -86400 seconds)
|
||||
$patch = new PropPatch([
|
||||
'{http://nextcloud.com/ns}default-alarm' => -86400
|
||||
]);
|
||||
$this->backend->updateCalendar($calendarId, $patch);
|
||||
$patch->commit();
|
||||
|
||||
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
|
||||
$this->assertEquals(-86400, $calendars[0]['{http://nextcloud.com/ns}default-alarm']);
|
||||
|
||||
// Test setting to "none"
|
||||
$patch = new PropPatch([
|
||||
'{http://nextcloud.com/ns}default-alarm' => null
|
||||
]);
|
||||
$this->backend->updateCalendar($calendarId, $patch);
|
||||
$patch->commit();
|
||||
|
||||
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
|
||||
$this->assertEquals(null, $calendars[0]['{http://nextcloud.com/ns}default-alarm']);
|
||||
|
||||
// Clean up
|
||||
$this->backend->deleteCalendar($calendars[0]['id'], true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
404
apps/dav/tests/unit/CalDAV/Federation/FederatedCalendarTest.php
Normal file
404
apps/dav/tests/unit/CalDAV/Federation/FederatedCalendarTest.php
Normal file
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -42,8 +42,8 @@ OC.L10N.register(
|
||||
"The lookup server is only available for global scale." : "Сервер пошуку доступний тільки для глобального масштабу.",
|
||||
"Search global and public address book for people" : "Шукати користувачів у глобальній та публічній адресних книгах",
|
||||
"Allow people to publish their data to a global and public address book" : "Дозволити користувачам розміщувати власні дані у глобальній публічній адресній книзі",
|
||||
"Trusted federation" : "Довірена федерація",
|
||||
"Automatically accept shares from trusted federated accounts and groups by default" : "Типово автоматично приймати пропозиції спільного доступу від надійних облікових записів та груп об'єднаних хмар",
|
||||
"Trusted federation" : "Довірені об'єднані хмари",
|
||||
"Automatically accept shares from trusted federated accounts and groups by default" : "Стандартно автоматично приймати пропозиції спільного доступу від надійних облікових записів та груп об'єднаних хмар",
|
||||
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Поділітися зі мною через мій #Nextcloud Federated Cloud ID, див. {url}",
|
||||
"Share with me through my #Nextcloud Federated Cloud ID" : "Поділітися зі мною через мій #Nextcloud Federated Cloud ID",
|
||||
"Share with me via Nextcloud" : "Поділіться зі мною у Nextcloud",
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
"The lookup server is only available for global scale." : "Сервер пошуку доступний тільки для глобального масштабу.",
|
||||
"Search global and public address book for people" : "Шукати користувачів у глобальній та публічній адресних книгах",
|
||||
"Allow people to publish their data to a global and public address book" : "Дозволити користувачам розміщувати власні дані у глобальній публічній адресній книзі",
|
||||
"Trusted federation" : "Довірена федерація",
|
||||
"Automatically accept shares from trusted federated accounts and groups by default" : "Типово автоматично приймати пропозиції спільного доступу від надійних облікових записів та груп об'єднаних хмар",
|
||||
"Trusted federation" : "Довірені об'єднані хмари",
|
||||
"Automatically accept shares from trusted federated accounts and groups by default" : "Стандартно автоматично приймати пропозиції спільного доступу від надійних облікових записів та груп об'єднаних хмар",
|
||||
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Поділітися зі мною через мій #Nextcloud Federated Cloud ID, див. {url}",
|
||||
"Share with me through my #Nextcloud Federated Cloud ID" : "Поділітися зі мною через мій #Nextcloud Federated Cloud ID",
|
||||
"Share with me via Nextcloud" : "Поділіться зі мною у Nextcloud",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,30 +128,30 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
|
||||
$share->setSharedWith($cloudId->getId());
|
||||
|
||||
try {
|
||||
$remoteShare = $this->getShareFromExternalShareTable($share);
|
||||
$remoteShare = $this->getShareFromExternalShareTable($share->getShareOwner(), $share->getTarget());
|
||||
} catch (ShareNotFound $e) {
|
||||
$remoteShare = null;
|
||||
}
|
||||
|
||||
if ($remoteShare) {
|
||||
try {
|
||||
$ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']);
|
||||
$shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time(), $shareType, $expirationDate);
|
||||
$share->setId($shareId);
|
||||
[$token, $remoteId] = $this->askOwnerToReShare($shareWith, $share, $shareId);
|
||||
// remote share was create successfully if we get a valid token as return
|
||||
$send = is_string($token) && $token !== '';
|
||||
} catch (\Exception $e) {
|
||||
// fall back to old re-share behavior if the remote server
|
||||
// doesn't support flat re-shares (was introduced with Nextcloud 9.1)
|
||||
$this->removeShareFromTable($share);
|
||||
$shareId = $this->createFederatedShare($share);
|
||||
}
|
||||
if ($send) {
|
||||
$ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']);
|
||||
$shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time(), $shareType, $expirationDate);
|
||||
[$token, $remoteId] = $this->notifications->requestReShare(
|
||||
$remoteShare['share_token'],
|
||||
$remoteShare['remote_id'],
|
||||
$shareId,
|
||||
$remoteShare['remote'],
|
||||
$shareWith,
|
||||
$permissions,
|
||||
$share->getNode()->getName(),
|
||||
$shareType,
|
||||
);
|
||||
// remote share was create successfully if we get a valid token as return
|
||||
if (is_string($token) && $token !== '') {
|
||||
$this->updateSuccessfulReshare($shareId, $token);
|
||||
$this->storeRemoteId($shareId, $remoteId);
|
||||
} else {
|
||||
$this->removeShareFromTable($share);
|
||||
$this->removeShareFromTable($shareId);
|
||||
$message_t = $this->l->t('File is already shared with %s', [$shareWith]);
|
||||
throw new \Exception($message_t);
|
||||
}
|
||||
@@ -216,7 +216,7 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
|
||||
}
|
||||
|
||||
if ($failure) {
|
||||
$this->removeShareFromTableById($shareId);
|
||||
$this->removeShareFromTable($shareId);
|
||||
$message_t = $this->l->t('Sharing %1$s failed, could not find %2$s, maybe the server is currently unreachable or uses a self-signed certificate.',
|
||||
[$share->getNode()->getName(), $share->getSharedWith()]);
|
||||
throw new \Exception($message_t);
|
||||
@@ -225,45 +225,17 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
|
||||
return $shareId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $shareWith
|
||||
* @param IShare $share
|
||||
* @param string $shareId internal share Id
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function askOwnerToReShare($shareWith, IShare $share, $shareId) {
|
||||
$remoteShare = $this->getShareFromExternalShareTable($share);
|
||||
$token = $remoteShare['share_token'];
|
||||
$remoteId = $remoteShare['remote_id'];
|
||||
$remote = $remoteShare['remote'];
|
||||
|
||||
[$token, $remoteId] = $this->notifications->requestReShare(
|
||||
$token,
|
||||
$remoteId,
|
||||
$shareId,
|
||||
$remote,
|
||||
$shareWith,
|
||||
$share->getPermissions(),
|
||||
$share->getNode()->getName(),
|
||||
$share->getShareType(),
|
||||
);
|
||||
|
||||
return [$token, $remoteId];
|
||||
}
|
||||
|
||||
/**
|
||||
* get federated share from the share_external table but exclude mounted link shares
|
||||
*
|
||||
* @param IShare $share
|
||||
* @return array
|
||||
* @throws ShareNotFound
|
||||
*/
|
||||
protected function getShareFromExternalShareTable(IShare $share) {
|
||||
protected function getShareFromExternalShareTable(string $owner, string $target) {
|
||||
$query = $this->dbConnection->getQueryBuilder();
|
||||
$query->select('*')->from($this->externalShareTable)
|
||||
->where($query->expr()->eq('user', $query->createNamedParameter($share->getShareOwner())))
|
||||
->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($share->getTarget())));
|
||||
->where($query->expr()->eq('user', $query->createNamedParameter($owner)))
|
||||
->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($target)));
|
||||
$qResult = $query->executeQuery();
|
||||
$result = $qResult->fetchAllAssociative();
|
||||
$qResult->closeCursor();
|
||||
@@ -329,14 +301,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.
|
||||
*
|
||||
@@ -453,7 +440,7 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
|
||||
|
||||
// only remove the share when all messages are send to not lose information
|
||||
// about the share to early
|
||||
$this->removeShareFromTable($share);
|
||||
$this->removeShareFromTable($share->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,13 +453,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 {
|
||||
@@ -483,19 +465,10 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* remove share from table
|
||||
*
|
||||
* @param IShare $share
|
||||
*/
|
||||
public function removeShareFromTable(IShare $share) {
|
||||
$this->removeShareFromTableById($share->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove share from table.
|
||||
*/
|
||||
private function removeShareFromTableById(string $shareId): void {
|
||||
public function removeShareFromTable(string $shareId): void {
|
||||
$qb = $this->dbConnection->getQueryBuilder();
|
||||
$qb->delete('share')
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)))
|
||||
|
||||
@@ -382,7 +382,7 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
|
||||
* @throws ShareNotFound
|
||||
*/
|
||||
protected function executeDeclineShare(IShare $share): void {
|
||||
$this->federatedShareProvider->removeShareFromTable($share);
|
||||
$this->federatedShareProvider->removeShareFromTable($share->getId());
|
||||
|
||||
$user = $this->getCorrectUser($share);
|
||||
|
||||
@@ -420,7 +420,7 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
|
||||
$share = $this->federatedShareProvider->getShareById($id);
|
||||
|
||||
$this->verifyShare($share, $token);
|
||||
$this->federatedShareProvider->removeShareFromTable($share);
|
||||
$this->federatedShareProvider->removeShareFromTable($share->getId());
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -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(7);
|
||||
$this->connection
|
||||
->expects($queryBuilderMatcher)
|
||||
->method('getQueryBuilder')
|
||||
->willReturnCallback(function () use ($queryBuilderMatcher, $qb1, $qb2, $qb3, $qb4, $qb5, $qb6) {
|
||||
return match ($queryBuilderMatcher->numberOfInvocations()) {
|
||||
1, 2 => $qb1,
|
||||
3 => $qb2,
|
||||
4 => $qb3,
|
||||
5 => $qb4,
|
||||
6 => $qb5,
|
||||
7 => $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);
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setExpirationDate($expirationDate)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
|
||||
$this->tokenHandler->method('generateToken')->willReturn('token');
|
||||
|
||||
@@ -215,7 +216,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
|
||||
$this->tokenHandler->method('generateToken')->willReturn('token');
|
||||
|
||||
@@ -268,7 +270,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
|
||||
$this->tokenHandler->method('generateToken')->willReturn('token');
|
||||
|
||||
@@ -359,7 +362,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
|
||||
$this->tokenHandler->method('generateToken')->willReturn('token');
|
||||
|
||||
@@ -431,7 +435,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setExpirationDate(new \DateTime('2019-02-01T01:02:03'))
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
|
||||
$this->tokenHandler->method('generateToken')->willReturn('token');
|
||||
$this->addressHandler->expects($this->any())->method('generateRemoteURL')
|
||||
@@ -450,11 +455,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());
|
||||
|
||||
@@ -508,7 +509,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
$this->provider->create($share);
|
||||
|
||||
$share2 = $this->shareManager->newShare();
|
||||
@@ -517,7 +519,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
$this->provider->create($share2);
|
||||
|
||||
$shares = $this->provider->getSharesBy('sharedBy', IShare::TYPE_REMOTE, null, false, -1, 0);
|
||||
@@ -552,7 +555,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
$this->provider->create($share);
|
||||
|
||||
$node2 = $this->getMockBuilder(File::class)->getMock();
|
||||
@@ -565,7 +569,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node2);
|
||||
->setNode($node2)
|
||||
->setTarget('');
|
||||
$this->provider->create($share2);
|
||||
|
||||
$shares = $this->provider->getSharesBy('sharedBy', IShare::TYPE_REMOTE, $node2, false, -1, 0);
|
||||
@@ -599,7 +604,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
$this->provider->create($share);
|
||||
|
||||
$share2 = $this->shareManager->newShare();
|
||||
@@ -608,7 +614,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
$this->provider->create($share2);
|
||||
|
||||
$shares = $this->provider->getSharesBy('shareOwner', IShare::TYPE_REMOTE, null, true, -1, 0);
|
||||
@@ -649,7 +656,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
$this->provider->create($share);
|
||||
|
||||
$share2 = $this->shareManager->newShare();
|
||||
@@ -658,7 +666,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner('shareOwner')
|
||||
->setPermissions(19)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($node);
|
||||
->setNode($node)
|
||||
->setTarget('');
|
||||
$this->provider->create($share2);
|
||||
|
||||
$shares = $this->provider->getSharesBy('shareOwner', IShare::TYPE_REMOTE, null, true, 1, 1);
|
||||
@@ -839,7 +848,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner($u1->getUID())
|
||||
->setPermissions(Constants::PERMISSION_READ)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($file1);
|
||||
->setNode($file1)
|
||||
->setTarget('');
|
||||
$this->provider->create($share1);
|
||||
|
||||
$share2 = $this->shareManager->newShare();
|
||||
@@ -848,7 +858,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner($u1->getUID())
|
||||
->setPermissions(Constants::PERMISSION_READ)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($file2);
|
||||
->setNode($file2)
|
||||
->setTarget('');
|
||||
$this->provider->create($share2);
|
||||
|
||||
$result = $this->provider->getSharesInFolder($u1->getUID(), $folder1, false);
|
||||
@@ -899,7 +910,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner($u1->getUID())
|
||||
->setPermissions(Constants::PERMISSION_READ)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($file1);
|
||||
->setNode($file1)
|
||||
->setTarget('');
|
||||
$this->provider->create($share1);
|
||||
|
||||
$share2 = $this->shareManager->newShare();
|
||||
@@ -908,7 +920,8 @@ class FederatedShareProviderTest extends \Test\TestCase {
|
||||
->setShareOwner($u1->getUID())
|
||||
->setPermissions(Constants::PERMISSION_READ)
|
||||
->setShareType(IShare::TYPE_REMOTE)
|
||||
->setNode($file1);
|
||||
->setNode($file1)
|
||||
->setTarget('');
|
||||
$this->provider->create($share2);
|
||||
|
||||
$result = $this->provider->getAccessList([$file1], true);
|
||||
|
||||
@@ -11,7 +11,18 @@ OC.L10N.register(
|
||||
"Federation" : "Об'єднання",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів.",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів. Так, це може бути корисно для автоматичної підстановки зовнішніх користувачів під час надання у спільний доступ ресурсів об'єднаних хмар.",
|
||||
"Could not add trusted server. Please try again later." : "Не вдалося додати довірений сервер. Спробуйте ще раз пізніше.",
|
||||
"Add trusted server" : "Додати довірений сервер",
|
||||
"Server url" : " Посилання на сервер",
|
||||
"Add" : "Додати",
|
||||
"Server ok" : "Сервер ОК",
|
||||
"User list was exchanged at least once successfully with the remote server." : "Принаймні один раз відбувся обмін списком користувачів з віддаленим сервером.",
|
||||
"Server pending" : "Очікування сервера",
|
||||
"Waiting for shared secret or initial user list exchange." : "Очікування парольної фрази спільного доступу або початкового обміну списком користувачів",
|
||||
"Server access revoked" : "Відкликано доступ для сервера",
|
||||
"Server failure" : "Помилка на стороні сервера",
|
||||
"Connection to the remote server failed or the remote server is misconfigured." : "Не вдалося встановити з'єднання з віддаленим сервером, або віддалений сервер має помилки з налаштуванням",
|
||||
"Failed to delete trusted server. Please try again later." : "Не вдалося вилучити довірений сервер. Спробуйте ще раз пізніше.",
|
||||
"Delete" : "Видалити",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів. Так, це може бути корисно для автоматичної підстановки зовнішніх користувачів під час надання у спільний доступ ресурсів об'єднаних хмар. Необов'язково додавати сервер яко довірений для створення спільного ресурсу між об'єднаними хмарами.",
|
||||
"Each server must validate the other. This process may require a few cron cycles." : "Кожен сервер має підтвердити один одного. Цей процес може вимагати кількох циклів виконання cron.",
|
||||
|
||||
@@ -9,7 +9,18 @@
|
||||
"Federation" : "Об'єднання",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів.",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів. Так, це може бути корисно для автоматичної підстановки зовнішніх користувачів під час надання у спільний доступ ресурсів об'єднаних хмар.",
|
||||
"Could not add trusted server. Please try again later." : "Не вдалося додати довірений сервер. Спробуйте ще раз пізніше.",
|
||||
"Add trusted server" : "Додати довірений сервер",
|
||||
"Server url" : " Посилання на сервер",
|
||||
"Add" : "Додати",
|
||||
"Server ok" : "Сервер ОК",
|
||||
"User list was exchanged at least once successfully with the remote server." : "Принаймні один раз відбувся обмін списком користувачів з віддаленим сервером.",
|
||||
"Server pending" : "Очікування сервера",
|
||||
"Waiting for shared secret or initial user list exchange." : "Очікування парольної фрази спільного доступу або початкового обміну списком користувачів",
|
||||
"Server access revoked" : "Відкликано доступ для сервера",
|
||||
"Server failure" : "Помилка на стороні сервера",
|
||||
"Connection to the remote server failed or the remote server is misconfigured." : "Не вдалося встановити з'єднання з віддаленим сервером, або віддалений сервер має помилки з налаштуванням",
|
||||
"Failed to delete trusted server. Please try again later." : "Не вдалося вилучити довірений сервер. Спробуйте ще раз пізніше.",
|
||||
"Delete" : "Видалити",
|
||||
"Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share." : "Об'єднання хмар дозволяє з'єднуватися з іншими довіреними серверами й обмінюватися обліковими даними користувачів. Так, це може бути корисно для автоматичної підстановки зовнішніх користувачів під час надання у спільний доступ ресурсів об'єднаних хмар. Необов'язково додавати сервер яко довірений для створення спільного ресурсу між об'єднаними хмарами.",
|
||||
"Each server must validate the other. This process may require a few cron cycles." : "Кожен сервер має підтвердити один одного. Цей процес може вимагати кількох циклів виконання cron.",
|
||||
|
||||
@@ -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;");
|
||||
|
||||
@@ -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;"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user