├── .editorconfig ├── .eslintrc.cjs ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report_form.yml │ ├── bug_report_form.yml.license │ ├── config.yml │ ├── feature_request_form.yml │ └── feature_request_form.yml.license └── workflows │ ├── build.yml │ ├── conventional_commits.yml │ ├── dependabot-approve-merge.yml │ ├── fixup.yml │ ├── lint.yml │ ├── npm-audit-fix.yml │ ├── npm-test.yml │ └── reuse.yml ├── .gitignore ├── AUTHORS.md ├── CHANGELOG.md ├── LICENSE ├── LICENSES ├── AGPL-3.0-or-later.txt ├── CC0-1.0.txt └── MIT.txt ├── Makefile ├── README.md ├── REUSE.toml ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── debug.js ├── errors │ ├── attachError.js │ ├── networkRequestAbortedError.js │ ├── networkRequestClientError.js │ ├── networkRequestError.js │ ├── networkRequestHttpError.js │ └── networkRequestServerError.js ├── index.js ├── models │ ├── addressBook.js │ ├── addressBookHome.js │ ├── calendar.js │ ├── calendarHome.js │ ├── calendarTrashBin.js │ ├── davCollection.js │ ├── davCollectionPublishable.js │ ├── davCollectionShareable.js │ ├── davEvent.js │ ├── davEventListener.js │ ├── davObject.js │ ├── deletedCalendar.js │ ├── principal.js │ ├── scheduleInbox.js │ ├── scheduleOutbox.js │ ├── subscription.js │ ├── vcard.js │ └── vobject.js ├── parser.js ├── propset │ ├── addressBookPropSet.js │ ├── calendarPropSet.js │ ├── davCollectionPropSet.js │ ├── principalPropSet.js │ └── scheduleInboxPropSet.js ├── request.js └── utility │ ├── namespaceUtility.js │ ├── stringUtility.js │ └── xmlUtility.js ├── test ├── assets │ └── unit │ │ └── parser │ │ └── dprop.xml ├── mocks │ ├── davCollection.mock.js │ └── request.mock.js └── unit │ ├── clientTest.js │ ├── debugTest.js │ ├── models │ ├── addressBookHomeTest.js │ ├── addressBookTest.js │ ├── calendarHomeTest.js │ ├── calendarTest.js │ ├── davCollectionPublishableTest.js │ ├── davCollectionShareableTest.js │ ├── davCollectionTest.js │ ├── davEventListenerTest.js │ ├── davEventTest.js │ ├── davObjectTest.js │ ├── principalTest.js │ ├── scheduleInboxTest.js │ ├── scheduleOutboxTest.js │ ├── subscriptionTest.js │ ├── vcardTest.js │ └── vobjectTest.js │ ├── parserTest.js │ ├── propset │ ├── addressBookPropSetTest.js │ ├── calendarPropSetTest.js │ ├── davCollectionPropSetTest.js │ ├── principalPropSet.js │ └── scheduleInboxPropSetTest.js │ ├── requestTest.js │ └── utility │ ├── namespaceUtilityTest.js │ ├── stringUtilityTest.js │ └── xmlUtilityTest.js └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | root = true 4 | # General 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | [*.{js,php}] 10 | indent_style = tab 11 | insert_final_newline = true 12 | [Makefile] 13 | indent_style = tab 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | module.exports = { 7 | extends: [ 8 | '@nextcloud', 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | /dist/*.js binary 4 | /dist/*.js.map binary 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Package maintainers 2 | * @hamza221 @st3iny 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_form.yml: -------------------------------------------------------------------------------- 1 | name: "🐞 Bug report" 2 | description: "Help us to improve by reporting a bug" 3 | labels: ["bug", "0. to triage"] 4 | body: 5 | - type: textarea 6 | id: reproduce 7 | attributes: 8 | label: Steps to reproduce 9 | description: | 10 | Describe the steps to reproduce the bug. 11 | The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ answer. 12 | value: | 13 | 1. 14 | 2. 15 | 3. 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: Expected-behavior 20 | attributes: 21 | label: Expected behavior 22 | description: | 23 | Tell us what should happen 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: actual-behavior 28 | attributes: 29 | label: Actual behavior 30 | description: Tell us what happens instead 31 | validations: 32 | required: true 33 | - type: input 34 | id: library-version 35 | attributes: 36 | label: Library version 37 | - type: textarea 38 | id: additional-info 39 | attributes: 40 | label: Additional info 41 | description: Any additional information related to the issue (ex. browser console errors, software versions). 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_form.yml.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | contact_links: 4 | - name: 🚨 Report a security or privacy issue 5 | url: https://hackerone.com/nextcloud 6 | about: Report security and privacy related issues privately to the Nextcloud team, so we can coordinate the fix and release without potentially exposing all Nextcloud servers and users in the meantime. 7 | - name: ❓ Community Support and Help 8 | url: https://help.nextcloud.com/ 9 | about: Configuration, webserver/proxy or performance issues and other questions 10 | - name: 💼 Nextcloud Enterprise 11 | url: https://portal.nextcloud.com/ 12 | about: If you are a Nextcloud Enterprise customer, or need Professional support, so it can be resolved directly by our dedicated engineers more quickly 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_form.yml: -------------------------------------------------------------------------------- 1 | name: "🚀 Feature request" 2 | description: "Suggest an idea for this library" 3 | labels: ["enhancement", "0. to triage"] 4 | body: 5 | - type: textarea 6 | id: description-problem 7 | attributes: 8 | label: Is your feature request related to a problem? Please describe. 9 | description: | 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | - type: textarea 12 | id: description-solution 13 | attributes: 14 | label: Describe the solution you'd like 15 | description: | 16 | A clear and concise description of what you want to happen. 17 | - type: textarea 18 | id: description-alternatives 19 | attributes: 20 | label: Describe alternatives you've considered 21 | description: | 22 | A clear and concise description of any alternative solutions or features you've considered. 23 | - type: textarea 24 | id: additional-context 25 | attributes: 26 | label: Additional context 27 | description: | 28 | Add any other context or screenshots about the feature request here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_form.yml.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: MIT 3 | name: Build 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | node: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: ['20'] 18 | 19 | name: node${{ matrix.node-version }} 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 22 | 23 | - name: Set up node ${{ matrix.node-version }} 24 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | 31 | - name: Build 32 | run: npm run build 33 | -------------------------------------------------------------------------------- /.github/workflows/conventional_commits.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: MIT 3 | name: Conventional Commits 4 | 5 | on: 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Conventional Commits 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 15 | - uses: webiny/action-conventional-commits@8bc41ff4e7d423d56fa4905f6ff79209a78776c7 # v1.3.0 16 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-approve-merge.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Dependabot 10 | 11 | on: 12 | pull_request_target: 13 | branches: 14 | - main 15 | - master 16 | - stable* 17 | 18 | permissions: 19 | contents: read 20 | 21 | concurrency: 22 | group: dependabot-approve-merge-${{ github.head_ref || github.run_id }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | auto-approve-merge: 27 | if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]' 28 | runs-on: ubuntu-latest-low 29 | permissions: 30 | # for hmarr/auto-approve-action to approve PRs 31 | pull-requests: write 32 | 33 | steps: 34 | - name: Disabled on forks 35 | if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} 36 | run: | 37 | echo 'Can not approve PRs from forks' 38 | exit 1 39 | 40 | # GitHub actions bot approve 41 | - uses: hmarr/auto-approve-action@b40d6c9ed2fa10c9a2749eca7eb004418a705501 # v2 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Nextcloud bot approve and merge request 46 | - uses: ahmadnassri/action-dependabot-auto-merge@45fc124d949b19b6b8bf6645b6c9d55f4f9ac61a # v2 47 | with: 48 | target: minor 49 | github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }} 50 | -------------------------------------------------------------------------------- /.github/workflows/fixup.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Block fixup and squash commits 10 | 11 | on: 12 | pull_request: 13 | types: [opened, ready_for_review, reopened, synchronize] 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: fixup-${{ github.head_ref || github.run_id }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | commit-message-check: 24 | if: github.event.pull_request.draft == false 25 | 26 | permissions: 27 | pull-requests: write 28 | name: Block fixup and squash commits 29 | 30 | runs-on: ubuntu-latest-low 31 | 32 | steps: 33 | - name: Run check 34 | uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2 35 | with: 36 | repo-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 20210 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: MIT 3 | name: Lint 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | node: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: ['20'] 18 | 19 | name: node${{ matrix.node-version }} 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 22 | 23 | - name: Set up node ${{ matrix.node-version }} 24 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | 31 | - name: Lint 32 | run: npm run lint 33 | -------------------------------------------------------------------------------- /.github/workflows/npm-audit-fix.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Npm audit fix and compile 10 | 11 | on: 12 | workflow_dispatch: 13 | schedule: 14 | # At 2:30 on Sundays 15 | - cron: '30 2 * * 0' 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | branches: ['main'] 25 | 26 | name: npm-audit-fix-${{ matrix.branches }} 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 31 | with: 32 | ref: ${{ matrix.branches }} 33 | 34 | - name: Read package.json node and npm engines version 35 | uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 36 | id: versions 37 | with: 38 | fallbackNode: '^20' 39 | fallbackNpm: '^10' 40 | 41 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 42 | uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 43 | with: 44 | node-version: ${{ steps.versions.outputs.nodeVersion }} 45 | 46 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 47 | run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' 48 | 49 | - name: Fix npm audit 50 | id: npm-audit 51 | uses: nextcloud-libraries/npm-audit-action@2a60bd2e79cc77f2cc4d9a3fe40f1a69896f3a87 # v0.1.0 52 | 53 | - name: Run npm ci and npm run build 54 | if: always() 55 | env: 56 | CYPRESS_INSTALL_BINARY: 0 57 | run: | 58 | npm ci 59 | npm run build --if-present 60 | 61 | - name: Create Pull Request 62 | if: always() 63 | uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 64 | with: 65 | token: ${{ secrets.COMMAND_BOT_PAT }} 66 | commit-message: 'fix(deps): Fix npm audit' 67 | committer: GitHub 68 | author: nextcloud-command 69 | signoff: true 70 | branch: automated/noid/${{ matrix.branches }}-fix-npm-audit 71 | title: '[${{ matrix.branches }}] Fix npm audit' 72 | body: ${{ steps.npm-audit.outputs.markdown }} 73 | labels: | 74 | dependencies 75 | 3. to review 76 | -------------------------------------------------------------------------------- /.github/workflows/npm-test.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: MIT 3 | name: Test 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | jest: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: ['20'] 19 | 20 | name: node${{ matrix.node-version }} 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 23 | 24 | - name: Set up node ${{ matrix.node-version }} 25 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Install Playwright Browsers 33 | run: npx playwright install --with-deps webkit chromium firefox 34 | 35 | - name: Test 36 | run: npm run test:coverage 37 | 38 | - name: Upload coverage reports to Codecov with GitHub Action 39 | uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5 40 | env: 41 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 7 | # 8 | # SPDX-License-Identifier: CC0-1.0 9 | 10 | name: REUSE Compliance Check 11 | 12 | on: [pull_request] 13 | 14 | jobs: 15 | reuse-compliance-check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | persist-credentials: false 22 | 23 | - name: REUSE Compliance Check 24 | uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | ### Intellij ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 5 | 6 | ## Directory-based project format 7 | .idea/ 8 | /*.iml 9 | # if you remove the above rule, at least ignore user-specific stuff: 10 | # .idea/workspace.xml 11 | # .idea/tasks.xml 12 | # .idea/dictionaries 13 | # and these sensitive or high-churn files: 14 | # .idea/dataSources.ids 15 | # .idea/dataSources.xml 16 | # .idea/sqlDataSources.xml 17 | # .idea/dynamic.xml 18 | # and, if using gradle:: 19 | # .idea/gradle.xml 20 | # .idea/libraries 21 | 22 | ## File-based project format 23 | *.ipr 24 | *.iws 25 | 26 | ## Additional for IntelliJ 27 | out/ 28 | 29 | # generated by mpeltonen/sbt-idea plugin 30 | .idea_modules/ 31 | 32 | # generated by JIRA plugin 33 | atlassian-ide-plugin.xml 34 | 35 | # generated by Crashlytics plugin (for Android Studio and Intellij) 36 | com_crashlytics_export_strings.xml 37 | 38 | 39 | ### OSX ### 40 | .DS_Store 41 | .AppleDouble 42 | .LSOverride 43 | 44 | # Icon must end with two \r 45 | Icon 46 | 47 | # Documentation 48 | docs/ 49 | 50 | 51 | # Thumbnails 52 | ._* 53 | 54 | # Files that might appear on external disk 55 | .Spotlight-V100 56 | .Trashes 57 | 58 | # Directories potentially created on remote AFP share 59 | .AppleDB 60 | .AppleDesktop 61 | Network Trash Folder 62 | Temporary Items 63 | .apdisk 64 | 65 | ### Sass ### 66 | build/.sass-cache/ 67 | 68 | ### Composer ### 69 | composer.phar 70 | vendor/ 71 | 72 | # vim ex mode 73 | .vimrc 74 | 75 | # kdevelop 76 | .kdev 77 | *.kdev4 78 | 79 | build/ 80 | node_modules/ 81 | *.clover 82 | 83 | # just sane ignores 84 | .*.sw[po] 85 | *.bak 86 | *.BAK 87 | *~ 88 | *.orig 89 | *.class 90 | .cvsignore 91 | Thumbs.db 92 | *.py[co] 93 | _darcs/* 94 | CVS/* 95 | .svn/* 96 | RCS/* 97 | 98 | /.project 99 | 100 | coverage/ 101 | dist/ 102 | 103 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | 5 | # Authors 6 | 7 | - Andy Scherzinger 8 | - Anna Larch 9 | - Christoph Wurst 10 | - Daniel 11 | - Georg Ehrke 12 | - Greta 13 | - John Molakvoæ 14 | - Julia Kirschenheuter <6078378+JuliaKirschenheuter@users.noreply.github.com> 15 | - Raimund Schlüßler 16 | - Richard Steinmetz 17 | - Roeland Jago Douma 18 | - Thomas Citharel 19 | - Tobias Speicher 20 | - William J. Bowman 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 5 | # Changelog 6 | 7 | All notable changes to this project will be documented in this file. 8 | 9 | ## v2.1.0 - 2025-05-30 10 | ### Added 11 | - Send custom default headers with each request 12 | ### Fixed 13 | - Send OPTIONS request to acquire advertised DAV features 14 | 15 | ## v2.0.0 - 2025-05-07 16 | ### Breaking changes 17 | - Removed the ability to pass a custom `xhrProvider` 18 | - Removed `beforeRequestHandler` and `afterRequestHandler` callbacks from the `Request` class 19 | ### Added 20 | - Pass custom abort signals to instances of the `Request` class 21 | - Use `@nextcloud/axios` for requests (instead of raw XHR code) 22 | - Migrate testing code to vitest 23 | ### Fixed 24 | - Serialize namespaced attributes consistently across browsers 25 | - Update vulnerable dependencies 26 | 27 | ## v1.5.3 - 2025-03-22 28 | ### Fixed 29 | - Update vulnerable dependencies 30 | 31 | ## v1.5.2 - 2024-10-14 32 | ### Fixed 33 | - Update vulnerable dependencies 34 | 35 | ## v1.5.1 - 2024-07-17 36 | ### Fixed 37 | - Fix serialization of schedule-calendar-transp property 38 | 39 | ## v1.5.0 - 2024-07-16 40 | ### Added 41 | - Expose scheduling transparency property 42 | ### Changed 43 | - Dependency updates 44 | 45 | ## v1.4.0 - 2024-06-10 46 | ### Added 47 | - Find principal collections 48 | - Ship an additional ESM bundle 49 | ### Changed 50 | - Bundle with vite instead of webpack 51 | - Dependency updates 52 | 53 | ## v1.3.0 - 2024-02-29 54 | ### Added 55 | - Implement updating a principal's schedule-default-calendar-URL 56 | ### Changed 57 | - Update node engines to next LTS (node 20 / npm 9) 58 | - Dependency updates 59 | 60 | ## v1.2.0 61 | ### Changed 62 | - Dependency updates 63 | ### Fixed 64 | - Type annotations 65 | 66 | ## v1.1.0 67 | ### Changed 68 | - Dependency updates 69 | - Resource search against display name and email. Now only the email is searched. 70 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | all: dev-setup build-js-production 4 | 5 | # Setup dev env 6 | dev-setup: clean clean-dev npm-init 7 | 8 | # Setup npm 9 | npm-init: 10 | npm install 11 | 12 | npm-update: 13 | npm update 14 | 15 | # Build 16 | build-js: 17 | npm run dev 18 | 19 | build-js-production: 20 | npm run build 21 | 22 | watch-js: 23 | npm run watch 24 | 25 | # Linting 26 | lint: 27 | npm run lint 28 | 29 | lint-fix: 30 | npm run lint:fix 31 | 32 | # Cleanup 33 | clean: 34 | rm -f dist/dist.js 35 | rm -f dist/dist.js.map 36 | 37 | clean-dev: 38 | rm -rf node_modules 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 | # cdav-library 6 | 7 | [![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/cdav-library)](https://api.reuse.software/info/github.com/nextcloud/cdav-library) 8 | [![NPM Version](https://img.shields.io/npm/v/%40nextcloud%2Fcdav-library)](https://www.npmjs.com/package/@nextcloud/cdav-library) 9 | 10 | :date: 📇 CalDAV and CardDAV client library for JavaScript 11 | 12 | ## Build the library 13 | 14 | ``` bash 15 | # install dependencies 16 | npm install 17 | 18 | # build for dev and watch changes 19 | npm run watch 20 | 21 | # build for dev 22 | npm run dev 23 | 24 | # build for production with minification 25 | npm run build 26 | 27 | ``` 28 | ## Running tests 29 | You can use the provided npm command to run all tests by using: 30 | 31 | ``` 32 | npm run test 33 | ``` 34 | 35 | ## :v: Code of conduct 36 | 37 | The Nextcloud community has core values that are shared between all members during conferences, 38 | hackweeks and on all interactions in online platforms including [Github](https://github.com/nextcloud) and [Forums](https://help.nextcloud.com). 39 | If you contribute, participate or interact with this community, please respect [our shared values](https://nextcloud.com/code-of-conduct/). :relieved: 40 | 41 | ## :heart: How to create a pull request 42 | 43 | This guide will help you get started: 44 | - :dancer: :smile: [Opening a pull request](https://opensource.guide/how-to-contribute/#opening-a-pull-request) 45 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | version = 1 4 | SPDX-PackageName = "cdav-library" 5 | SPDX-PackageSupplier = "Nextcloud " 6 | SPDX-PackageDownloadLocation = "https://github.com/nextcloud/cdav-library" 7 | 8 | [[annotations]] 9 | path = ["package.json", "package-lock.json", "test/assets/unit/parser/dprop.xml"] 10 | precedence = "aggregate" 11 | SPDX-FileCopyrightText = "2018 Nextcloud GmbH and Nextcloud contributors" 12 | SPDX-License-Identifier = "AGPL-3.0-or-later" 13 | 14 | [[annotations]] 15 | path = [".github/CODEOWNERS", "renovate.json"] 16 | precedence = "aggregate" 17 | SPDX-FileCopyrightText = "2024 Nextcloud GmbH and Nextcloud contributors" 18 | SPDX-License-Identifier = "AGPL-3.0-or-later" 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nextcloud/cdav-library", 3 | "version": "2.1.0", 4 | "description": "CalDAV and CardDAV client library for Nextcloud", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.mjs", 10 | "require": "./dist/index.cjs" 11 | } 12 | }, 13 | "files": [ 14 | "README.md", 15 | "CHANGELOG.md", 16 | "LICENSE", 17 | "dist" 18 | ], 19 | "scripts": { 20 | "build": "vite build --mode=production", 21 | "dev": "vite build --mode=development", 22 | "watch": "vite build --mode=development --watch", 23 | "test": "vitest run", 24 | "test:coverage": "vitest run --coverage", 25 | "test:watch": "vitest", 26 | "lint": "eslint --ext .js src", 27 | "lint:fix": "eslint --ext .js src --fix" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/nextcloud/cdav-library.git" 32 | }, 33 | "keywords": [ 34 | "caldav", 35 | "carddav", 36 | "nextcloud", 37 | "rfc4791", 38 | "rfc6352" 39 | ], 40 | "author": "Nextcloud GmbH and Nextcloud contributors", 41 | "license": "AGPL-3.0-or-later", 42 | "bugs": { 43 | "url": "https://github.com/nextcloud/cdav-library/issues" 44 | }, 45 | "homepage": "https://github.com/nextcloud/cdav-library#readme", 46 | "browserslist": [ 47 | "extends @nextcloud/browserslist-config" 48 | ], 49 | "engines": { 50 | "node": "^20.0.0", 51 | "npm": "^10.0.0" 52 | }, 53 | "devDependencies": { 54 | "@nextcloud/browserslist-config": "^3.0.1", 55 | "@nextcloud/eslint-config": "^8.4.2", 56 | "@nextcloud/vite-config": "^2.3.5", 57 | "@vitest/browser": "^3.0.6", 58 | "@vitest/coverage-istanbul": "^3.1.1", 59 | "playwright": "^1.49.1", 60 | "vite": "^6.3.5", 61 | "vitest": "^3.0.6" 62 | }, 63 | "dependencies": { 64 | "@nextcloud/axios": "^2.5.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "helpers:pinGitHubActionDigests", 6 | ":dependencyDashboard", 7 | ":semanticCommits", 8 | ":gitSignOff" 9 | ], 10 | "timezone": "Europe/Berlin", 11 | "schedule": [ 12 | "before 5am on tuesday" 13 | ], 14 | "labels": [ 15 | "dependencies", 16 | "3. to review" 17 | ], 18 | "commitMessageAction": "Bump", 19 | "commitMessageTopic": "{{depName}}", 20 | "commitMessageExtra": "from {{currentVersion}} to {{#if isPinDigest}}{{{newDigestShort}}}{{else}}{{#if isMajor}}{{prettyNewMajor}}{{else}}{{#if isSingleVersion}}{{prettyNewVersion}}{{else}}{{#if newValue}}{{{newValue}}}{{else}}{{{newDigestShort}}}{{/if}}{{/if}}{{/if}}{{/if}}", 21 | "rangeStrategy": "bump", 22 | "rebaseWhen": "conflicted", 23 | "ignoreUnstable": false, 24 | "baseBranches": [ 25 | "main" 26 | ], 27 | "enabledManagers": [ 28 | "github-actions", 29 | "npm" 30 | ], 31 | "ignoreDeps": [ 32 | "node", 33 | "npm", 34 | "postcss-loader" 35 | ], 36 | "packageRules": [ 37 | { 38 | "description": "Request JavaScript reviews", 39 | "matchManagers": ["npm"], 40 | "reviewers": [ 41 | "st3iny"] 42 | }, 43 | { 44 | "description": "Bump Github actions monthly and request reviews", 45 | "matchManagers": ["github-actions"], 46 | "extends": ["schedule:monthly"], 47 | "reviewers": [ 48 | "st3iny"] 49 | }, 50 | { 51 | "matchUpdateTypes": ["minor", "patch"], 52 | "matchCurrentVersion": "!/^0/", 53 | "automerge": true, 54 | "automergeType": "pr", 55 | "platformAutomerge": true, 56 | "labels": [ 57 | "dependencies", 58 | "4. to release" 59 | ], 60 | "reviewers": [] 61 | }, 62 | { 63 | "matchBaseBranches": ["main"], 64 | "matchDepTypes": ["devDependencies"], 65 | "extends": ["schedule:monthly"] 66 | }, 67 | { 68 | "groupName": "Jest family", 69 | "matchPackageNames": [ 70 | "jest", 71 | "jest-environment-jsdom", 72 | "babel-jest", 73 | "@vue/vue2-jest", 74 | "@types/jest" 75 | ], 76 | "automerge": true 77 | } 78 | ], 79 | "vulnerabilityAlerts": { 80 | "enabled": true, 81 | "semanticCommitType": "fix", 82 | "schedule": "before 7am every weekday", 83 | "dependencyDashboardApproval": false, 84 | "commitMessageSuffix": "" 85 | }, 86 | "osvVulnerabilityAlerts": true 87 | } 88 | -------------------------------------------------------------------------------- /src/debug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | /** 11 | * creates a debug function bound to a context 12 | * @param {string} context 13 | * @return {Function} 14 | */ 15 | export function debugFactory(context) { 16 | return (...args) => { 17 | if (debugFactory.enabled) { 18 | // eslint-disable-next-line no-console 19 | console.debug(context, ...args) 20 | } 21 | } 22 | } 23 | 24 | debugFactory.enabled = false 25 | -------------------------------------------------------------------------------- /src/errors/attachError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | /** 11 | * Generic error class that allows attaching more than just a message 12 | * 13 | * @abstract 14 | */ 15 | export default class AttachError extends Error { 16 | 17 | /** 18 | * 19 | * @param {object} attach 20 | */ 21 | constructor(attach) { 22 | super() 23 | 24 | Object.assign(this, attach) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/errors/networkRequestAbortedError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import AttachError from './attachError.js' 11 | 12 | export default class NetworkRequestAbortedError extends AttachError {} 13 | -------------------------------------------------------------------------------- /src/errors/networkRequestClientError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import NetworkRequestHttpError from './networkRequestHttpError.js' 11 | 12 | export default class NetworkRequestClientError extends NetworkRequestHttpError {} 13 | -------------------------------------------------------------------------------- /src/errors/networkRequestError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import AttachError from './attachError.js' 11 | 12 | export default class NetworkRequestError extends AttachError {} 13 | -------------------------------------------------------------------------------- /src/errors/networkRequestHttpError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import AttachError from './attachError.js' 11 | 12 | export default class NetworkRequestHttpError extends AttachError {} 13 | -------------------------------------------------------------------------------- /src/errors/networkRequestServerError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import NetworkRequestHttpError from './networkRequestHttpError.js' 11 | 12 | export default class NetworkRequestServerError extends NetworkRequestHttpError {} 13 | -------------------------------------------------------------------------------- /src/models/addressBook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { davCollectionShareable } from './davCollectionShareable.js' 11 | import { DavCollection } from './davCollection.js' 12 | import * as NS from '../utility/namespaceUtility.js' 13 | import * as StringUtility from '../utility/stringUtility.js' 14 | import * as XMLUtility from '../utility/xmlUtility.js' 15 | import addressBookPropSet from '../propset/addressBookPropSet.js' 16 | import { VCard } from './vcard.js' 17 | 18 | import { debugFactory } from '../debug.js' 19 | const debug = debugFactory('AddressBook') 20 | 21 | /** 22 | * This class represents an address book collection as specified in 23 | * https://tools.ietf.org/html/rfc6352#section-5.2 24 | * 25 | * On top of all the properties provided by davCollectionShareable and DavCollection, 26 | * It allows you access to the following list of properties: 27 | * - description 28 | * - enabled 29 | * - readOnly 30 | * 31 | * The first two allowing read-write access 32 | * 33 | * @augments DavCollection 34 | */ 35 | export class AddressBook extends davCollectionShareable(DavCollection) { 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | constructor(...args) { 41 | super(...args) 42 | 43 | super._registerObjectFactory('text/vcard', VCard) 44 | super._registerPropSetFactory(addressBookPropSet) 45 | 46 | super._exposeProperty('description', NS.IETF_CARDDAV, 'addressbook-description', true) 47 | super._exposeProperty('enabled', NS.OWNCLOUD, 'enabled', true) 48 | super._exposeProperty('readOnly', NS.OWNCLOUD, 'read-only') 49 | } 50 | 51 | /** 52 | * finds all VCards in this address book 53 | * 54 | * @return {Promise} 55 | */ 56 | findAllVCards() { 57 | return super.findAllByFilter((elm) => elm instanceof VCard) 58 | } 59 | 60 | /** 61 | * finds all contacts in an address-book, but with filtered data. 62 | * 63 | * Example use: 64 | * findAllAndFilterBySimpleProperties(['EMAIL', 'UID', 'CATEGORIES', 'FN', 'TEL', 'NICKNAME', 'N']) 65 | * 66 | * @param {string[]} props 67 | * @return {Promise} 68 | */ 69 | async findAllAndFilterBySimpleProperties(props) { 70 | const children = [] 71 | props.forEach((prop) => { 72 | children.push({ 73 | name: [NS.IETF_CARDDAV, 'prop'], 74 | attributes: [['name', prop]], 75 | }) 76 | }) 77 | 78 | return this.addressbookQuery(null, [{ 79 | name: [NS.DAV, 'getetag'], 80 | }, { 81 | name: [NS.DAV, 'getcontenttype'], 82 | }, { 83 | name: [NS.DAV, 'resourcetype'], 84 | }, { 85 | name: [NS.IETF_CARDDAV, 'address-data'], 86 | children, 87 | }, { 88 | name: [NS.NEXTCLOUD, 'has-photo'], 89 | }]) 90 | } 91 | 92 | /** 93 | * creates a new VCard object in this address book 94 | * 95 | * @param {string} data 96 | * @return {Promise} 97 | */ 98 | async createVCard(data) { 99 | debug('creating VCard object') 100 | 101 | const name = StringUtility.uid('', 'vcf') 102 | const headers = { 103 | 'Content-Type': 'text/vcard; charset=utf-8', 104 | } 105 | 106 | return super.createObject(name, headers, data) 107 | } 108 | 109 | /** 110 | * sends an addressbook query as defined in 111 | * https://tools.ietf.org/html/rfc6352#section-8.6 112 | * 113 | * @param {object[]} filter 114 | * @param {object[]} prop 115 | * @param {number} limit 116 | * @param {string} test Either anyof or allof 117 | * @return {Promise} 118 | */ 119 | async addressbookQuery(filter, prop = null, limit = null, test = 'anyof') { 120 | debug('sending an addressbook-query request') 121 | 122 | const [skeleton] = XMLUtility.getRootSkeleton( 123 | [NS.IETF_CARDDAV, 'addressbook-query'], 124 | ) 125 | 126 | if (!prop) { 127 | skeleton.children.push({ 128 | name: [NS.DAV, 'prop'], 129 | children: this._propFindList.map((p) => ({ name: p })), 130 | }) 131 | } else { 132 | skeleton.children.push({ 133 | name: [NS.DAV, 'prop'], 134 | children: prop, 135 | }) 136 | } 137 | 138 | // According to the spec, every address-book query needs a filter, 139 | // but Nextcloud just returns all elements without a filter. 140 | if (filter) { 141 | skeleton.children.push({ 142 | name: [NS.IETF_CARDDAV, 'filter'], 143 | attributes: [ 144 | ['test', test], 145 | ], 146 | children: filter, 147 | }) 148 | } 149 | 150 | if (limit) { 151 | skeleton.children.push({ 152 | name: [NS.IETF_CARDDAV, 'limit'], 153 | children: [{ 154 | name: [NS.IETF_CARDDAV, 'nresults'], 155 | value: limit, 156 | }], 157 | }) 158 | } 159 | 160 | const headers = { 161 | Depth: '1', 162 | } 163 | const body = XMLUtility.serialize(skeleton) 164 | const response = await this._request.report(this.url, headers, body) 165 | return super._handleMultiStatusResponse(response, AddressBook._isRetrievalPartial(prop)) 166 | } 167 | 168 | /** 169 | * sends an addressbook multiget query as defined in 170 | * https://tools.ietf.org/html/rfc6352#section-8.7 171 | * 172 | * @param {string[]} hrefs 173 | * @param {object[]} prop 174 | * @return {Promise} 175 | */ 176 | async addressbookMultiget(hrefs = [], prop) { 177 | debug('sending an addressbook-multiget request') 178 | 179 | if (hrefs.length === 0) { 180 | return [] 181 | } 182 | 183 | const headers = { 184 | Depth: '1', 185 | } 186 | const body = this._buildMultiGetBody(hrefs, prop) 187 | const response = await this._request.report(this.url, headers, body) 188 | return super._handleMultiStatusResponse(response, AddressBook._isRetrievalPartial(prop)) 189 | } 190 | 191 | /** 192 | * sends an addressbook multiget query as defined in 193 | * https://tools.ietf.org/html/rfc6352#section-8.7 194 | * and requests a download of the result 195 | * 196 | * @param {string[]} hrefs 197 | * @param {object[]} prop 198 | * @return {Promise<{body: string|object, status: number, headers: object}>} 199 | */ 200 | async addressbookMultigetExport(hrefs = [], prop) { 201 | debug('sending an addressbook-multiget request and request download') 202 | 203 | if (hrefs.length === 0) { 204 | return '' 205 | } 206 | 207 | const headers = { 208 | Depth: '1', 209 | } 210 | const body = this._buildMultiGetBody(hrefs, prop) 211 | return this._request.report(this.url + '?export', headers, body) 212 | } 213 | 214 | /** 215 | * 216 | * @param {string[]} hrefs 217 | * @param {object[]} prop 218 | * @return String 219 | * @private 220 | */ 221 | _buildMultiGetBody(hrefs, prop) { 222 | const [skeleton] = XMLUtility.getRootSkeleton( 223 | [NS.IETF_CARDDAV, 'addressbook-multiget'], 224 | ) 225 | 226 | if (!prop) { 227 | skeleton.children.push({ 228 | name: [NS.DAV, 'prop'], 229 | children: this._propFindList.map((p) => ({ name: p })), 230 | }) 231 | } else { 232 | skeleton.children.push({ 233 | name: [NS.DAV, 'prop'], 234 | children: prop, 235 | }) 236 | } 237 | 238 | hrefs.forEach((href) => { 239 | skeleton.children.push({ 240 | name: [NS.DAV, 'href'], 241 | value: href, 242 | }) 243 | }) 244 | 245 | return XMLUtility.serialize(skeleton) 246 | } 247 | 248 | /** 249 | * @inheritDoc 250 | */ 251 | static getPropFindList() { 252 | return super.getPropFindList().concat([ 253 | [NS.IETF_CARDDAV, 'addressbook-description'], 254 | [NS.IETF_CARDDAV, 'supported-address-data'], 255 | [NS.IETF_CARDDAV, 'max-resource-size'], 256 | [NS.CALENDARSERVER, 'getctag'], 257 | [NS.OWNCLOUD, 'enabled'], 258 | [NS.OWNCLOUD, 'read-only'], 259 | ]) 260 | } 261 | 262 | /** 263 | * checks if the prop part of a report requested partial data 264 | * 265 | * @param {object[]} prop 266 | * @return {boolean} 267 | * @private 268 | */ 269 | static _isRetrievalPartial(prop) { 270 | if (!prop) { 271 | return false 272 | } 273 | 274 | const addressBookDataProperty = prop.find((p) => { 275 | return p.name[0] === NS.IETF_CARDDAV && p.name[1] === 'address-data' 276 | }) 277 | 278 | if (!addressBookDataProperty) { 279 | return false 280 | } 281 | 282 | return !!addressBookDataProperty.children 283 | } 284 | 285 | } 286 | -------------------------------------------------------------------------------- /src/models/addressBookHome.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { DavCollection } from './davCollection.js' 11 | import * as NS from '../utility/namespaceUtility.js' 12 | import { AddressBook } from './addressBook.js' 13 | 14 | import { debugFactory } from '../debug.js' 15 | const debug = debugFactory('AddressBookHome') 16 | 17 | /** 18 | * This class represents an address book home as specified in 19 | * https://tools.ietf.org/html/rfc6352#section-7.1.1 20 | * 21 | * As of this versions' release, the Nextcloud server will always 22 | * return only one address book home. Despite that, RFC6352 allows 23 | * a server to return multiple address book homes though. 24 | */ 25 | export class AddressBookHome extends DavCollection { 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | constructor(...args) { 31 | super(...args) 32 | 33 | super._registerCollectionFactory('{' + NS.IETF_CARDDAV + '}addressbook', AddressBook) 34 | } 35 | 36 | /** 37 | * finds all address books in this address book home 38 | * 39 | * @return {Promise} 40 | */ 41 | async findAllAddressBooks() { 42 | return super.findAllByFilter((elm) => elm instanceof AddressBook) 43 | } 44 | 45 | /** 46 | * creates a new address book collection 47 | * 48 | * @param {string} displayname 49 | * @return {Promise} 50 | */ 51 | async createAddressBookCollection(displayname) { 52 | debug('creating an addressbook collection') 53 | 54 | const props = [{ 55 | name: [NS.DAV, 'resourcetype'], 56 | children: [{ 57 | name: [NS.DAV, 'collection'], 58 | }, { 59 | name: [NS.IETF_CARDDAV, 'addressbook'], 60 | }], 61 | }, { 62 | name: [NS.DAV, 'displayname'], 63 | value: displayname, 64 | }] 65 | 66 | const name = super._getAvailableNameFromToken(displayname) 67 | return super.createCollection(name, props) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/models/calendar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { DavCollection } from './davCollection.js' 11 | import { davCollectionPublishable } from './davCollectionPublishable.js' 12 | import { davCollectionShareable } from './davCollectionShareable.js' 13 | import { VObject } from './vobject.js' 14 | import calendarPropSet from '../propset/calendarPropSet.js' 15 | import * as NS from '../utility/namespaceUtility.js' 16 | import * as StringUtility from '../utility/stringUtility.js' 17 | import * as XMLUtility from '../utility/xmlUtility.js' 18 | 19 | import { debugFactory } from '../debug.js' 20 | const debug = debugFactory('Calendar') 21 | 22 | /** 23 | * This class represents an calendar collection as specified in 24 | * https://tools.ietf.org/html/rfc4791#section-4.2 25 | * 26 | * On top of all the properties provided by davCollectionShareable, 27 | * davCollectionPublishable and DavCollection, 28 | * It allows you access to the following list of properties: 29 | * - color 30 | * - enabled 31 | * - order 32 | * - timezone 33 | * - components 34 | * 35 | * The first four allowing read-write access 36 | * 37 | * @augments DavCollection 38 | */ 39 | export class Calendar extends davCollectionPublishable(davCollectionShareable(DavCollection)) { 40 | 41 | /** 42 | * @inheritDoc 43 | */ 44 | constructor(...args) { 45 | super(...args) 46 | 47 | super._registerObjectFactory('text/calendar', VObject) 48 | super._registerPropSetFactory(calendarPropSet) 49 | 50 | super._exposeProperty('color', NS.APPLE, 'calendar-color', true) 51 | super._exposeProperty('enabled', NS.OWNCLOUD, 'calendar-enabled', true) 52 | super._exposeProperty('order', NS.APPLE, 'calendar-order', true) 53 | super._exposeProperty('timezone', NS.IETF_CALDAV, 'calendar-timezone', true) 54 | super._exposeProperty('components', NS.IETF_CALDAV, 'supported-calendar-component-set') 55 | super._exposeProperty('transparency', NS.IETF_CALDAV, 'schedule-calendar-transp', true) 56 | } 57 | 58 | /** 59 | * finds all VObjects in this calendar 60 | * 61 | * @return {Promise} 62 | */ 63 | async findAllVObjects() { 64 | return super.findAllByFilter((elm) => elm instanceof VObject) 65 | } 66 | 67 | /** 68 | * find all VObjects filtered by type 69 | * 70 | * @param {string} type 71 | * @return {Promise} 72 | */ 73 | async findByType(type) { 74 | return this.calendarQuery([{ 75 | name: [NS.IETF_CALDAV, 'comp-filter'], 76 | attributes: [ 77 | ['name', 'VCALENDAR'], 78 | ], 79 | children: [{ 80 | name: [NS.IETF_CALDAV, 'comp-filter'], 81 | attributes: [ 82 | ['name', type], 83 | ], 84 | }], 85 | }]) 86 | } 87 | 88 | /** 89 | * find all VObjects in a time-range filtered by type 90 | * 91 | * @param {number} type 92 | * @param {Date} from 93 | * @param {Date} to 94 | * @return {Promise} 95 | */ 96 | async findByTypeInTimeRange(type, from, to) { 97 | return this.calendarQuery([{ 98 | name: [NS.IETF_CALDAV, 'comp-filter'], 99 | attributes: [ 100 | ['name', 'VCALENDAR'], 101 | ], 102 | children: [{ 103 | name: [NS.IETF_CALDAV, 'comp-filter'], 104 | attributes: [ 105 | ['name', type], 106 | ], 107 | children: [{ 108 | name: [NS.IETF_CALDAV, 'time-range'], 109 | attributes: [ 110 | ['start', Calendar._getICalendarDateTimeFromDateObject(from)], 111 | ['end', Calendar._getICalendarDateTimeFromDateObject(to)], 112 | ], 113 | }], 114 | }], 115 | }]) 116 | } 117 | 118 | /** 119 | * create a VObject inside this calendar 120 | * 121 | * @param data 122 | * @return {Promise} 123 | */ 124 | async createVObject(data) { 125 | const name = StringUtility.uid('', 'ics') 126 | const headers = { 127 | 'Content-Type': 'text/calendar; charset=utf-8', 128 | } 129 | 130 | return super.createObject(name, headers, data) 131 | } 132 | 133 | /** 134 | * sends a calendar query as defined in 135 | * https://tools.ietf.org/html/rfc4791#section-7.8 136 | * 137 | * @param {object[]} filter 138 | * @param {object[]} prop 139 | * @param {string} timezone 140 | * @return {Promise} 141 | */ 142 | async calendarQuery(filter, prop = null, timezone = null) { 143 | debug('sending an calendar-query request') 144 | 145 | const [skeleton] = XMLUtility.getRootSkeleton( 146 | [NS.IETF_CALDAV, 'calendar-query'], 147 | ) 148 | 149 | if (!prop) { 150 | skeleton.children.push({ 151 | name: [NS.DAV, 'prop'], 152 | children: this._propFindList.map((p) => ({ name: p })), 153 | }) 154 | } else { 155 | skeleton.children.push({ 156 | name: [NS.DAV, 'prop'], 157 | children: prop, 158 | }) 159 | } 160 | 161 | // According to the spec, every calendar-query needs a filter, 162 | // but Nextcloud just returns all elements without a filter. 163 | if (filter) { 164 | skeleton.children.push({ 165 | name: [NS.IETF_CALDAV, 'filter'], 166 | children: filter, 167 | }) 168 | } 169 | 170 | if (timezone) { 171 | skeleton.children.push({ 172 | name: [NS.IETF_CALDAV, 'timezone'], 173 | value: timezone, 174 | }) 175 | } 176 | 177 | const headers = { 178 | Depth: '1', 179 | } 180 | 181 | const body = XMLUtility.serialize(skeleton) 182 | const response = await this._request.report(this.url, headers, body) 183 | return super._handleMultiStatusResponse(response, Calendar._isRetrievalPartial(prop)) 184 | } 185 | 186 | /** 187 | * sends a calendar multiget query as defined in 188 | * https://tools.ietf.org/html/rfc4791#section-7.9 189 | * 190 | * @param {string[]} hrefs 191 | * @param {object[]} prop 192 | * @return {Promise} 193 | */ 194 | async calendarMultiget(hrefs = [], prop) { 195 | debug('sending an calendar-multiget request') 196 | 197 | if (hrefs.length === 0) { 198 | return [] 199 | } 200 | 201 | const [skeleton] = XMLUtility.getRootSkeleton( 202 | [NS.IETF_CALDAV, 'calendar-multiget'], 203 | ) 204 | 205 | if (!prop) { 206 | skeleton.children.push({ 207 | name: [NS.DAV, 'prop'], 208 | children: this._propFindList.map((p) => ({ name: p })), 209 | }) 210 | } else { 211 | skeleton.children.push({ 212 | name: [NS.DAV, 'prop'], 213 | children: prop, 214 | }) 215 | } 216 | 217 | hrefs.forEach((href) => { 218 | skeleton.children.push({ 219 | name: [NS.DAV, 'href'], 220 | value: href, 221 | }) 222 | }) 223 | 224 | const headers = { 225 | Depth: '1', 226 | } 227 | const body = XMLUtility.serialize(skeleton) 228 | const response = await this._request.report(this.url, headers, body) 229 | return super._handleMultiStatusResponse(response, Calendar._isRetrievalPartial(prop)) 230 | } 231 | 232 | /** 233 | * sends a calendar free-busy-query as defined in 234 | * https://tools.ietf.org/html/rfc4791#section-7.10 235 | * 236 | * @param {Date} from 237 | * @param {Date} to 238 | * @return {Promise} 239 | */ 240 | async freeBusyQuery(from, to) { 241 | /* eslint-disable no-tabs */ 242 | // debug('sending a free-busy-query request'); 243 | // 244 | // const [skeleton] = XMLUtility.getRootSkeleton( 245 | // [NS.IETF_CALDAV, 'free-busy-query'], 246 | // [NS.IETF_CALDAV, 'time-range'] 247 | // ); 248 | // 249 | // skeleton[0][0].attributes.push(['start', Calendar._getICalendarDateTimeFromDateObject(from)]); 250 | // skeleton[0][0].attributes.push(['end', Calendar._getICalendarDateTimeFromDateObject(to)]); 251 | // 252 | // const headers = { 253 | // 'Depth': '1' 254 | // }; 255 | // const body = XMLUtility.serialize(skeleton); 256 | // const response = await this._request.report(this.url, headers, body); 257 | /* eslint-enable no-tabs */ 258 | 259 | // TODO - finish implementation 260 | } 261 | 262 | /** 263 | * @inheritDoc 264 | */ 265 | static getPropFindList() { 266 | return super.getPropFindList().concat([ 267 | [NS.APPLE, 'calendar-order'], 268 | [NS.APPLE, 'calendar-color'], 269 | [NS.CALENDARSERVER, 'getctag'], 270 | [NS.IETF_CALDAV, 'calendar-description'], 271 | [NS.IETF_CALDAV, 'calendar-timezone'], 272 | [NS.IETF_CALDAV, 'supported-calendar-component-set'], 273 | [NS.IETF_CALDAV, 'supported-calendar-data'], 274 | [NS.IETF_CALDAV, 'max-resource-size'], 275 | [NS.IETF_CALDAV, 'min-date-time'], 276 | [NS.IETF_CALDAV, 'max-date-time'], 277 | [NS.IETF_CALDAV, 'max-instances'], 278 | [NS.IETF_CALDAV, 'max-attendees-per-instance'], 279 | [NS.IETF_CALDAV, 'supported-collation-set'], 280 | [NS.IETF_CALDAV, 'calendar-free-busy-set'], 281 | [NS.IETF_CALDAV, 'schedule-calendar-transp'], 282 | [NS.IETF_CALDAV, 'schedule-default-calendar-URL'], 283 | [NS.OWNCLOUD, 'calendar-enabled'], 284 | [NS.NEXTCLOUD, 'owner-displayname'], 285 | [NS.NEXTCLOUD, 'trash-bin-retention-duration'], 286 | [NS.NEXTCLOUD, 'deleted-at'], 287 | ]) 288 | } 289 | 290 | /** 291 | * checks if the prop part of a report requested partial data 292 | * 293 | * @param {object[]} prop 294 | * @return {boolean} 295 | * @private 296 | */ 297 | static _isRetrievalPartial(prop) { 298 | if (!prop) { 299 | return false 300 | } 301 | 302 | const addressBookDataProperty = prop.find((p) => { 303 | return p.name[0] === NS.IETF_CALDAV && p.name[1] === 'calendar-data' 304 | }) 305 | 306 | if (!addressBookDataProperty) { 307 | return false 308 | } 309 | 310 | return !!addressBookDataProperty.children 311 | } 312 | 313 | /** 314 | * creates an iCalendar formatted DATE-TIME string from a date object 315 | * 316 | * @param {Date} date 317 | * @return {string} 318 | * @private 319 | */ 320 | static _getICalendarDateTimeFromDateObject(date) { 321 | return [ 322 | date.getUTCFullYear(), 323 | ('0' + (date.getUTCMonth() + 1)).slice(-2), 324 | ('0' + date.getUTCDate()).slice(-2), 325 | 'T', 326 | ('0' + date.getUTCHours()).slice(-2), 327 | ('0' + date.getUTCMinutes()).slice(-2), 328 | ('0' + date.getUTCSeconds()).slice(-2), 329 | 'Z', 330 | ].join('') 331 | } 332 | 333 | } 334 | -------------------------------------------------------------------------------- /src/models/calendarHome.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { DavCollection } from './davCollection.js' 11 | import { Calendar } from './calendar.js' 12 | import { Subscription } from './subscription.js' 13 | import ScheduleInbox from './scheduleInbox.js' 14 | import ScheduleOutbox from './scheduleOutbox.js' 15 | import * as NS from '../utility/namespaceUtility.js' 16 | import * as XMLUtility from '../utility/xmlUtility.js' 17 | 18 | import { debugFactory } from '../debug.js' 19 | import { CalendarTrashBin } from './calendarTrashBin.js' 20 | import { DeletedCalendar } from './deletedCalendar.js' 21 | const debug = debugFactory('CalendarHome') 22 | 23 | /** 24 | * This class represents a calendar home as specified in 25 | * https://tools.ietf.org/html/rfc4791#section-6.2.1 26 | * 27 | * As of this versions' release, the Nextcloud server will always 28 | * return only one calendar home. Despite that, RFC4791 allows 29 | * a server to return multiple calendar homes though. 30 | */ 31 | export class CalendarHome extends DavCollection { 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | constructor(...args) { 37 | super(...args) 38 | 39 | super._registerCollectionFactory('{' + NS.IETF_CALDAV + '}calendar', Calendar) 40 | super._registerCollectionFactory('{' + NS.NEXTCLOUD + '}deleted-calendar', DeletedCalendar) 41 | super._registerCollectionFactory('{' + NS.CALENDARSERVER + '}subscribed', Subscription) 42 | super._registerCollectionFactory('{' + NS.IETF_CALDAV + '}schedule-inbox', ScheduleInbox) 43 | super._registerCollectionFactory('{' + NS.IETF_CALDAV + '}schedule-outbox', ScheduleOutbox) 44 | super._registerCollectionFactory('{' + NS.NEXTCLOUD + '}trash-bin', CalendarTrashBin) 45 | } 46 | 47 | /** 48 | * Finds all CalDAV-specific collections in this calendar home 49 | * 50 | * @return {Promise<(Calendar|Subscription|ScheduleInbox|ScheduleOutbox|CalendarTrashBin|DeletedCalendar)[]>} 51 | */ 52 | async findAllCalDAVCollections() { 53 | return super.findAllByFilter((elm) => elm instanceof Calendar || elm instanceof CalendarTrashBin 54 | || elm instanceof Subscription || elm instanceof ScheduleInbox || elm instanceof ScheduleOutbox 55 | || elm instanceof DeletedCalendar) 56 | } 57 | 58 | /** 59 | * Finds all CalDAV-specific collections in this calendar home, grouped by type 60 | * 61 | * @return {Promise<{ 62 | calendars: Calendar[], 63 | deletedCalendars: DeletedCalendar[], 64 | trashBins: CalendarTrashBin[], 65 | subscriptions: Subscription[], 66 | scheduleInboxes: ScheduleInbox[], 67 | scheduleOutboxes: ScheduleOutbox[], 68 | }>} 69 | */ 70 | async findAllCalDAVCollectionsGrouped() { 71 | const collections = await super.findAll() 72 | 73 | return { 74 | calendars: collections.filter(c => c instanceof Calendar && !(c instanceof ScheduleInbox) && !(c instanceof Subscription) && !(c instanceof DeletedCalendar)), 75 | deletedCalendars: collections.filter(c => c instanceof DeletedCalendar), 76 | trashBins: collections.filter(c => c instanceof CalendarTrashBin), 77 | subscriptions: collections.filter(c => c instanceof Subscription), 78 | scheduleInboxes: collections.filter(c => c instanceof ScheduleInbox), 79 | scheduleOutboxes: collections.filter(c => c instanceof ScheduleOutbox), 80 | } 81 | } 82 | 83 | /** 84 | * finds all calendars in this calendar home 85 | * 86 | * @return {Promise} 87 | */ 88 | async findAllCalendars() { 89 | return super.findAllByFilter((elm) => elm instanceof Calendar && !(elm instanceof ScheduleInbox) && !(elm instanceof Subscription) && !(elm instanceof DeletedCalendar)) 90 | } 91 | 92 | /** 93 | * Finds all deleted calendars in this calendar home 94 | * 95 | * @return {Promise} 96 | */ 97 | async findAllDeletedCalendars() { 98 | return super.findAllByFilter((elm) => elm instanceof DeletedCalendar) 99 | } 100 | 101 | /** 102 | * finds all subscriptions in this calendar home 103 | * 104 | * @return {Promise} 105 | */ 106 | async findAllSubscriptions() { 107 | return super.findAllByFilter((elm) => elm instanceof Subscription) 108 | } 109 | 110 | /** 111 | * finds all schedule inboxes in this calendar home 112 | * 113 | * @return {Promise} 114 | */ 115 | async findAllScheduleInboxes() { 116 | return super.findAllByFilter((elm) => elm instanceof ScheduleInbox) 117 | } 118 | 119 | /** 120 | * finds all schedule outboxes in this calendar home 121 | * 122 | * @return {Promise} 123 | */ 124 | async findAllScheduleOutboxes() { 125 | return super.findAllByFilter((elm) => elm instanceof ScheduleOutbox) 126 | } 127 | 128 | /** 129 | * creates a new calendar collection 130 | * 131 | * @param {string} displayname 132 | * @param {string} color 133 | * @param {string[]} supportedComponentSet 134 | * @param {number} order 135 | * @param {string=} timezone 136 | * @return {Promise} 137 | */ 138 | async createCalendarCollection(displayname, color, supportedComponentSet = null, order = null, timezone = null) { 139 | debug('creating a calendar collection') 140 | 141 | const props = [{ 142 | name: [NS.DAV, 'resourcetype'], 143 | children: [{ 144 | name: [NS.DAV, 'collection'], 145 | }, { 146 | name: [NS.IETF_CALDAV, 'calendar'], 147 | }], 148 | }, { 149 | name: [NS.DAV, 'displayname'], 150 | value: displayname, 151 | }, { 152 | name: [NS.APPLE, 'calendar-color'], 153 | value: color, 154 | }, { 155 | name: [NS.OWNCLOUD, 'calendar-enabled'], 156 | value: '1', 157 | }] 158 | 159 | if (timezone) { 160 | props.push({ 161 | name: [NS.IETF_CALDAV, 'calendar-timezone'], 162 | value: timezone, 163 | }) 164 | } 165 | 166 | if (supportedComponentSet) { 167 | props.push({ 168 | name: [NS.IETF_CALDAV, 'supported-calendar-component-set'], 169 | children: supportedComponentSet.map((supportedComponent) => { 170 | return { 171 | name: [NS.IETF_CALDAV, 'comp'], 172 | attributes: [ 173 | ['name', supportedComponent], 174 | ], 175 | } 176 | }), 177 | }) 178 | } 179 | 180 | if (order) { 181 | props.push({ 182 | name: [NS.APPLE, 'calendar-order'], 183 | value: order, 184 | }) 185 | } 186 | 187 | const name = super._getAvailableNameFromToken(displayname) 188 | return super.createCollection(name, props) 189 | } 190 | 191 | /** 192 | * creates a new subscription 193 | * 194 | * @param {string} displayname 195 | * @param {string} color 196 | * @param {string} source 197 | * @param {number} order 198 | * @return {Promise} 199 | */ 200 | async createSubscribedCollection(displayname, color, source, order = null) { 201 | debug('creating a subscribed collection') 202 | 203 | const props = [{ 204 | name: [NS.DAV, 'resourcetype'], 205 | children: [{ 206 | name: [NS.DAV, 'collection'], 207 | }, { 208 | name: [NS.CALENDARSERVER, 'subscribed'], 209 | }], 210 | }, { 211 | name: [NS.DAV, 'displayname'], 212 | value: displayname, 213 | }, { 214 | name: [NS.APPLE, 'calendar-color'], 215 | value: color, 216 | }, { 217 | name: [NS.OWNCLOUD, 'calendar-enabled'], 218 | value: '1', 219 | }, { 220 | name: [NS.CALENDARSERVER, 'source'], 221 | children: [{ 222 | name: [NS.DAV, 'href'], 223 | value: source, 224 | }], 225 | }] 226 | 227 | if (order) { 228 | props.push({ 229 | name: [NS.APPLE, 'calendar-order'], 230 | value: order, 231 | }) 232 | } 233 | 234 | const name = super._getAvailableNameFromToken(displayname) 235 | return super.createCollection(name, props) 236 | } 237 | 238 | /** 239 | * Search all calendars the user has access to 240 | * This method makes use of Nextcloud's custom 241 | * calendar Search API 242 | * 243 | * Documentation about that API can be found at: ... 244 | * 245 | * @return {Promise} 246 | */ 247 | async search() { 248 | // TODO - implement me 249 | } 250 | 251 | /** 252 | * enables the birthday calendar for the Calendar Home that belongs to this user 253 | * 254 | * @return {Promise} 255 | */ 256 | async enableBirthdayCalendar() { 257 | const [skeleton] = XMLUtility.getRootSkeleton( 258 | [NS.NEXTCLOUD, 'enable-birthday-calendar'], 259 | ) 260 | const xmlBody = XMLUtility.serialize(skeleton) 261 | 262 | await this._request.post(this.url, {}, xmlBody) 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /src/models/calendarTrashBin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { DavCollection } from './davCollection.js' 11 | import * as NS from '../utility/namespaceUtility.js' 12 | import { VObject } from './vobject.js' 13 | import * as XMLUtility from '../utility/xmlUtility.js' 14 | 15 | export class CalendarTrashBin extends DavCollection { 16 | 17 | /** 18 | * @inheritDoc 19 | */ 20 | constructor(...args) { 21 | super(...args) 22 | 23 | super._registerObjectFactory('text/calendar', VObject) 24 | 25 | super._exposeProperty('retentionDuration', NS.NEXTCLOUD, 'trash-bin-retention-duration') 26 | } 27 | 28 | async findDeletedObjects() { 29 | const [skeleton] = XMLUtility.getRootSkeleton( 30 | [NS.IETF_CALDAV, 'calendar-query'], 31 | ) 32 | skeleton.children.push({ 33 | name: [NS.DAV, 'prop'], 34 | children: VObject.getPropFindList() 35 | .map((p) => ({ name: p })) 36 | .concat([ 37 | { name: [NS.NEXTCLOUD, 'calendar-uri'] }, 38 | { name: [NS.NEXTCLOUD, 'deleted-at'] }, 39 | ]), 40 | }) 41 | skeleton.children.push({ 42 | name: [NS.IETF_CALDAV, 'filter'], 43 | children: [{ 44 | name: [NS.IETF_CALDAV, 'comp-filter'], 45 | attributes: [ 46 | ['name', 'VCALENDAR'], 47 | ], 48 | children: [{ 49 | name: [NS.IETF_CALDAV, 'comp-filter'], 50 | attributes: [ 51 | ['name', 'VEVENT'], 52 | ], 53 | children: [], 54 | }], 55 | }], 56 | }) 57 | const headers = { 58 | Depth: '1', 59 | } 60 | const body = XMLUtility.serialize(skeleton) 61 | const response = await this._request.report(this._url + 'objects', headers, body) 62 | return super._handleMultiStatusResponse(response) 63 | } 64 | 65 | async restore(uri) { 66 | await this._request.move(uri, this._url + 'restore/file') 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/models/davCollection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import * as NS from '../utility/namespaceUtility.js' 11 | import * as StringUtility from '../utility/stringUtility.js' 12 | import * as XMLUtility from '../utility/xmlUtility.js' 13 | import DAVEventListener from './davEventListener.js' 14 | 15 | import { debugFactory } from '../debug.js' 16 | import davCollectionPropSet from '../propset/davCollectionPropSet.js' 17 | import { DavObject } from './davObject.js' 18 | const debug = debugFactory('DavCollection') 19 | 20 | export class DavCollection extends DAVEventListener { 21 | 22 | /** 23 | * @param {object} parent 24 | * @param {Request} request 25 | * @param {string} url 26 | * @param {object} props 27 | */ 28 | constructor(parent, request, url, props) { 29 | super() 30 | 31 | // This is a collection, so always make sure to end with a / 32 | if (url.slice(-1) !== '/') { 33 | url += '/' 34 | } 35 | 36 | Object.assign(this, { 37 | // parameters 38 | _parent: parent, 39 | _request: request, 40 | _url: url, 41 | _props: props, 42 | // constructors 43 | _collectionFactoryMapper: {}, 44 | _objectFactoryMapper: {}, 45 | // house keeping 46 | _updatedProperties: [], 47 | _childrenNames: [], 48 | 49 | // parsers / factories 50 | _propFindList: [], 51 | _propSetFactory: [], 52 | 53 | }) 54 | 55 | this._registerPropSetFactory(davCollectionPropSet) 56 | 57 | this._exposeProperty('displayname', NS.DAV, 'displayname', true) 58 | this._exposeProperty('owner', NS.DAV, 'owner') 59 | this._exposeProperty('resourcetype', NS.DAV, 'resourcetype') 60 | this._exposeProperty('syncToken', NS.DAV, 'sync-token') 61 | this._exposeProperty('currentUserPrivilegeSet', NS.DAV, 'current-user-privilege-set') 62 | 63 | Object.defineProperty(this, 'url', { 64 | get: () => this._url, 65 | }) 66 | 67 | this._propFindList.push(...DavObject.getPropFindList()) 68 | this._propFindList.push(...DavCollection.getPropFindList()) 69 | } 70 | 71 | /** 72 | * finds all children of a collection 73 | * 74 | * @return {Promise} 75 | */ 76 | async findAll() { 77 | const response = await this._request.propFind(this._url, this._propFindList, 1) 78 | return this._handleMultiStatusResponse(response, false) 79 | } 80 | 81 | /** 82 | * finds all children of a collection filtered by filter 83 | * 84 | * @param {Function} filter 85 | * @return {Promise} 86 | */ 87 | async findAllByFilter(filter) { 88 | const all = await this.findAll() 89 | return all.filter(filter) 90 | } 91 | 92 | /** 93 | * find one object by its uri 94 | * 95 | * @param {string} uri 96 | * @return {Promise} 97 | */ 98 | async find(uri) { 99 | const response = await this._request.propFind(this._url + uri, this._propFindList, 0) 100 | response.body = { [this._url + uri]: response.body } 101 | return this._handleMultiStatusResponse(response, false)[0] 102 | } 103 | 104 | /** 105 | * creates a new webdav collection 106 | * https://tools.ietf.org/html/rfc5689 107 | * 108 | * You usually don't want to call this method directly 109 | * but instead use: 110 | * - AddressBookHome->createAddressBookCollection 111 | * - CalendarHome->createCalendarCollection 112 | * - CalendarHome->createSubscribedCollection 113 | * 114 | * @param {string} name 115 | * @param {?Array} props 116 | * @return {Promise} 117 | */ 118 | async createCollection(name, props = null) { 119 | debug('creating a collection') 120 | 121 | if (!props) { 122 | props = [{ 123 | name: [NS.DAV, 'resourcetype'], 124 | children: [{ 125 | name: [NS.DAV, 'collection'], 126 | }], 127 | }] 128 | } 129 | 130 | const [skeleton, dPropChildren] = XMLUtility.getRootSkeleton( 131 | [NS.DAV, 'mkcol'], 132 | [NS.DAV, 'set'], 133 | [NS.DAV, 'prop'], 134 | ) 135 | 136 | dPropChildren.push(...props) 137 | 138 | const uri = this._getAvailableNameFromToken(name) 139 | const data = XMLUtility.serialize(skeleton) 140 | await this._request.mkCol(this.url + uri, {}, data) 141 | return this.find(uri + '/') 142 | } 143 | 144 | /** 145 | * creates a new webdav object inside this collection 146 | * 147 | * You usually don't want to call this method directly 148 | * but instead use: 149 | * - AddressBook->createVCard 150 | * - Calendar->createVObject 151 | * 152 | * @param {string} name 153 | * @param {object} headers 154 | * @param {string} data 155 | * @return {Promise} 156 | */ 157 | async createObject(name, headers, data) { 158 | debug('creating an object') 159 | 160 | await this._request.put(this.url + name, headers, data) 161 | return this.find(name) 162 | } 163 | 164 | /** 165 | * sends a PropPatch request to update the collections' properties 166 | * The request is only made if properties actually changed 167 | * 168 | * @return {Promise} 169 | */ 170 | async update() { 171 | if (this._updatedProperties.length === 0) { 172 | return 173 | } 174 | 175 | const properties = {} 176 | this._updatedProperties.forEach((updatedProperty) => { 177 | properties[updatedProperty] = this._props[updatedProperty] 178 | }) 179 | const propSet = this._propSetFactory.reduce((arr, p) => [...arr, ...p(properties)], []) 180 | 181 | const [skeleton, dPropSet] = XMLUtility.getRootSkeleton( 182 | [NS.DAV, 'propertyupdate'], 183 | [NS.DAV, 'set'], 184 | [NS.DAV, 'prop']) 185 | 186 | dPropSet.push(...propSet) 187 | 188 | const body = XMLUtility.serialize(skeleton) 189 | await this._request.propPatch(this._url, {}, body) 190 | } 191 | 192 | /** 193 | * deletes the DavCollection on the server 194 | * 195 | * @param {object} headers - additional HTTP headers to send 196 | * @return {Promise} 197 | */ 198 | async delete(headers = {}) { 199 | await this._request.delete(this._url, headers) 200 | } 201 | 202 | /** 203 | * 204 | * @return {boolean} 205 | */ 206 | isReadable() { 207 | return this.currentUserPrivilegeSet.includes('{DAV:}read') 208 | } 209 | 210 | /** 211 | * 212 | * @return {boolean} 213 | */ 214 | isWriteable() { 215 | return this.currentUserPrivilegeSet.includes('{DAV:}write') 216 | } 217 | 218 | /** 219 | * checks whether this is of the same type as another collection 220 | * @param {DavCollection} collection 221 | */ 222 | isSameCollectionTypeAs(collection) { 223 | const ownResourceType = this.resourcetype 224 | const foreignResourceType = collection.resourcetype 225 | 226 | const ownDiff = ownResourceType.find((r) => foreignResourceType.indexOf(r) === -1) 227 | const foreignDiff = foreignResourceType.find((r) => ownResourceType.indexOf(r) === -1) 228 | 229 | return ownDiff === undefined && foreignDiff === undefined 230 | } 231 | 232 | /** 233 | * @protected 234 | * @param {string} identifier 235 | * @param {Function} factory 236 | * @return void 237 | */ 238 | _registerCollectionFactory(identifier, factory) { 239 | this._collectionFactoryMapper[identifier] = factory 240 | if (typeof factory.getPropFindList === 'function') { 241 | this._propFindList.push(...factory.getPropFindList()) 242 | } 243 | } 244 | 245 | /** 246 | * @protected 247 | * @param {string} identifier 248 | * @param {Function} factory 249 | * @return void 250 | */ 251 | _registerObjectFactory(identifier, factory) { 252 | this._objectFactoryMapper[identifier] = factory 253 | if (typeof factory.getPropFindList === 'function') { 254 | this._propFindList.push(...factory.getPropFindList()) 255 | } 256 | } 257 | 258 | /** 259 | * @protected 260 | * @param factory 261 | * @return void 262 | */ 263 | _registerPropSetFactory(factory) { 264 | this._propSetFactory.push(factory) 265 | } 266 | 267 | /** 268 | * @protected 269 | * @param {string} localName 270 | * @param {string} xmlNamespace 271 | * @param {string} xmlName 272 | * @param {boolean} mutable 273 | * @return void 274 | */ 275 | _exposeProperty(localName, xmlNamespace, xmlName, mutable = false) { 276 | if (mutable) { 277 | Object.defineProperty(this, localName, { 278 | get: () => this._props[`{${xmlNamespace}}${xmlName}`], 279 | set: (val) => { 280 | this._props[`{${xmlNamespace}}${xmlName}`] = val 281 | if (this._updatedProperties.indexOf(`{${xmlNamespace}}${xmlName}`) === -1) { 282 | this._updatedProperties.push(`{${xmlNamespace}}${xmlName}`) 283 | } 284 | }, 285 | }) 286 | } else { 287 | Object.defineProperty(this, localName, { 288 | get: () => this._props[`{${xmlNamespace}}${xmlName}`], 289 | }) 290 | } 291 | } 292 | 293 | /** 294 | * @protected 295 | * @param {string} token 296 | * @return {string} 297 | */ 298 | _getAvailableNameFromToken(token) { 299 | return StringUtility.uri(token, name => { 300 | return this._childrenNames.indexOf(this._url + name) === -1 301 | && this._childrenNames.indexOf(this._url + name + '/') === -1 302 | }) 303 | } 304 | 305 | /** 306 | * get updated properties for this collection from server 307 | * @protected 308 | * @return {object} 309 | */ 310 | async _updatePropsFromServer() { 311 | const response = await this._request.propFind(this.url, this.constructor.getPropFindList()) 312 | this._props = response.body 313 | } 314 | 315 | /** 316 | * @param {object} response 317 | * @param {boolean} isPartial 318 | * @return {DavObject[]|DavCollection[]} 319 | * @protected 320 | */ 321 | _handleMultiStatusResponse(response, isPartial = false) { 322 | const index = [] 323 | const children = [] 324 | 325 | Object.entries(response.body).forEach(([path, props]) => { 326 | // The DAV Server will always return a propStat 327 | // block containing properties of the current url 328 | // we are not interested, so let's filter it out 329 | if (path === this._url || path + '/' === this.url) { 330 | return 331 | } 332 | 333 | index.push(path) 334 | const url = this._request.pathname(path) 335 | 336 | // empty resourcetype property => this is no collection 337 | if (((!props['{DAV:}resourcetype']) || (props['{DAV:}resourcetype'].length === 0)) && props['{DAV:}getcontenttype']) { 338 | debug(`${path} was identified as a file`) 339 | 340 | const contentType = props['{DAV:}getcontenttype'].split(';')[0] 341 | if (!this._objectFactoryMapper[contentType]) { 342 | debug(`No constructor for content-type ${contentType} (${path}) registered, treating as generic object`) 343 | children.push(new DavObject(this, this._request, url, props)) 344 | return 345 | } 346 | 347 | children.push(new this._objectFactoryMapper[contentType](this, this._request, url, props, isPartial)) 348 | } else { 349 | debug(`${path} was identified as a collection`) 350 | 351 | // get first collection type other than DAV collection 352 | const collectionType = props['{DAV:}resourcetype'].find((r) => { 353 | return r !== `{${NS.DAV}}collection` 354 | }) 355 | 356 | if (!collectionType) { 357 | debug(`Collection-type of ${path} was not specified, treating as generic collection`) 358 | children.push(new DavCollection(this, this._request, url, props)) 359 | return 360 | } 361 | if (!this._collectionFactoryMapper[collectionType]) { 362 | debug(`No constructor for collection-type ${collectionType} (${path}) registered, treating as generic collection`) 363 | children.push(new DavCollection(this, this._request, url, props)) 364 | return 365 | } 366 | 367 | children.push(new this._collectionFactoryMapper[collectionType](this, this._request, url, props)) 368 | } 369 | }) 370 | 371 | this._childrenNames.push(...index) 372 | return children 373 | } 374 | 375 | /** 376 | * A list of all property names that should be included 377 | * in propfind requests that may include this collection 378 | * 379 | * @return {string[][]} 380 | */ 381 | static getPropFindList() { 382 | return [ 383 | [NS.DAV, 'displayname'], 384 | [NS.DAV, 'owner'], 385 | [NS.DAV, 'resourcetype'], 386 | [NS.DAV, 'sync-token'], 387 | [NS.DAV, 'current-user-privilege-set'], 388 | ] 389 | } 390 | 391 | } 392 | -------------------------------------------------------------------------------- /src/models/davCollectionPublishable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import * as NS from '../utility/namespaceUtility.js' 11 | import * as XMLUtility from '../utility/xmlUtility.js' 12 | 13 | import { debugFactory } from '../debug.js' 14 | const debug = debugFactory('DavCollectionPublishable') 15 | 16 | export function davCollectionPublishable(Base) { 17 | return class extends Base { 18 | 19 | /** 20 | * @inheritDoc 21 | */ 22 | constructor(...args) { 23 | super(...args) 24 | 25 | super._exposeProperty('publishURL', NS.CALENDARSERVER, 'publish-url') 26 | } 27 | 28 | /** 29 | * publishes the DavCollection 30 | * 31 | * @return {Promise} 32 | */ 33 | async publish() { 34 | debug(`Publishing ${this.url}`) 35 | 36 | const [skeleton] = XMLUtility.getRootSkeleton( 37 | [NS.CALENDARSERVER, 'publish-calendar']) 38 | const xml = XMLUtility.serialize(skeleton) 39 | 40 | // TODO - ideally the server should return a 'pre-publish-url' as described in the standard 41 | 42 | await this._request.post(this._url, { 'Content-Type': 'application/xml; charset=utf-8' }, xml) 43 | await this._updatePropsFromServer() 44 | } 45 | 46 | /** 47 | * unpublishes the DavCollection 48 | * 49 | * @return {Promise} 50 | */ 51 | async unpublish() { 52 | debug(`Unpublishing ${this.url}`) 53 | 54 | const [skeleton] = XMLUtility.getRootSkeleton( 55 | [NS.CALENDARSERVER, 'unpublish-calendar']) 56 | const xml = XMLUtility.serialize(skeleton) 57 | 58 | await this._request.post(this._url, { 'Content-Type': 'application/xml; charset=utf-8' }, xml) 59 | delete this._props['{http://calendarserver.org/ns/}publish-url'] 60 | } 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | static getPropFindList() { 66 | return super.getPropFindList().concat([ 67 | [NS.CALENDARSERVER, 'publish-url'], 68 | ]) 69 | } 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/models/davCollectionShareable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import * as NS from '../utility/namespaceUtility.js' 11 | import * as XMLUtility from '../utility/xmlUtility.js' 12 | 13 | import { debugFactory } from '../debug.js' 14 | const debug = debugFactory('DavCollectionShareable') 15 | 16 | export function davCollectionShareable(Base) { 17 | return class extends Base { 18 | 19 | /** 20 | * @inheritDoc 21 | */ 22 | constructor(...args) { 23 | super(...args) 24 | 25 | super._exposeProperty('shares', NS.OWNCLOUD, 'invite') 26 | super._exposeProperty('allowedSharingModes', NS.CALENDARSERVER, 'allowed-sharing-modes') 27 | } 28 | 29 | /** 30 | * shares a DavCollection 31 | * 32 | * @param {string} principalScheme 33 | * @param {boolean} writeable 34 | * @param {string} summary 35 | * @return {Promise} 36 | */ 37 | async share(principalScheme, writeable = false, summary = '') { 38 | debug(`Sharing ${this.url} with ${principalScheme}`) 39 | const [skeleton, setProp] = XMLUtility.getRootSkeleton( 40 | [NS.OWNCLOUD, 'share'], [NS.OWNCLOUD, 'set']) 41 | 42 | setProp.push({ 43 | name: [NS.DAV, 'href'], 44 | value: principalScheme, 45 | }) 46 | 47 | if (writeable) { 48 | setProp.push({ 49 | name: [NS.OWNCLOUD, 'read-write'], 50 | }) 51 | } 52 | if (summary !== '') { 53 | setProp.push({ 54 | name: [NS.OWNCLOUD, 'summary'], 55 | value: summary, 56 | }) 57 | } 58 | 59 | const xml = XMLUtility.serialize(skeleton) 60 | return this._request.post(this._url, { 'Content-Type': 'application/xml; charset=utf-8' }, xml).then(() => { 61 | const index = this.shares.findIndex((e) => e.href === principalScheme) 62 | 63 | if (index === -1) { 64 | this.shares.push({ 65 | href: principalScheme, 66 | access: [writeable ? '{http://owncloud.org/ns}read-write' : '{http://owncloud.org/ns}read'], 67 | 'common-name': null, 68 | 'invite-accepted': true, 69 | }) 70 | } else { 71 | this.shares[index].access 72 | = [writeable ? '{http://owncloud.org/ns}read-write' : '{http://owncloud.org/ns}read'] 73 | } 74 | }) 75 | } 76 | 77 | /** 78 | * unshares a DAVCollection 79 | * 80 | * @param {string} principalScheme 81 | * @return {Promise} 82 | */ 83 | async unshare(principalScheme) { 84 | debug(`Unsharing ${this.url} with ${principalScheme}`) 85 | 86 | const [skeleton, oSetChildren] = XMLUtility.getRootSkeleton( 87 | [NS.OWNCLOUD, 'share'], [NS.OWNCLOUD, 'remove']) 88 | 89 | oSetChildren.push({ 90 | name: [NS.DAV, 'href'], 91 | value: principalScheme, 92 | }) 93 | 94 | const xml = XMLUtility.serialize(skeleton) 95 | return this._request.post(this._url, { 'Content-Type': 'application/xml; charset=utf-8' }, xml).then(() => { 96 | const index = this.shares.findIndex((e) => e.href === principalScheme) 97 | if (index === -1) { 98 | return 99 | } 100 | 101 | this.shares.splice(index, 1) 102 | }) 103 | } 104 | 105 | /** 106 | * checks whether a collection is shareable 107 | * 108 | * @return {boolean} 109 | */ 110 | isShareable() { 111 | if (!Array.isArray(this.allowedSharingModes)) { 112 | return false 113 | } 114 | 115 | return this.allowedSharingModes.includes(`{${NS.CALENDARSERVER}}can-be-shared`) 116 | } 117 | 118 | /** 119 | * checks whether a collection is publishable 120 | * 121 | * @return {boolean} 122 | */ 123 | isPublishable() { 124 | if (!Array.isArray(this.allowedSharingModes)) { 125 | return false 126 | } 127 | 128 | return this.allowedSharingModes.includes(`{${NS.CALENDARSERVER}}can-be-published`) 129 | } 130 | 131 | /** 132 | * @inheritDoc 133 | */ 134 | static getPropFindList() { 135 | return super.getPropFindList().concat([ 136 | [NS.OWNCLOUD, 'invite'], 137 | [NS.CALENDARSERVER, 'allowed-sharing-modes'], 138 | ]) 139 | } 140 | 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/models/davEvent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | export default class DAVEvent { 11 | 12 | /** 13 | * 14 | * @param {string} type 15 | * @param {object} options 16 | */ 17 | constructor(type, options = {}) { 18 | Object.assign(this, { 19 | type, 20 | }, options) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/models/davEventListener.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | export default class DAVEventListener { 11 | 12 | constructor() { 13 | this._eventListeners = {} 14 | } 15 | 16 | /** 17 | * adds an event listener 18 | * 19 | * @param {string} type 20 | * @param {Function} listener 21 | * @param {object} options 22 | */ 23 | addEventListener(type, listener, options = null) { 24 | this._eventListeners[type] = this._eventListeners[type] || [] 25 | this._eventListeners[type].push({ listener, options }) 26 | } 27 | 28 | /** 29 | * removes an event listener 30 | * 31 | * @param {string} type 32 | * @param {Function} dListener 33 | */ 34 | removeEventListener(type, dListener) { 35 | if (!this._eventListeners[type]) { 36 | return 37 | } 38 | 39 | const index = this._eventListeners[type] 40 | .findIndex(({ listener }) => listener === dListener) 41 | if (index === -1) { 42 | return 43 | } 44 | this._eventListeners[type].splice(index, 1) 45 | } 46 | 47 | /** 48 | * dispatch event on object 49 | * 50 | * @param {string} type 51 | * @param {DAVEvent} event 52 | */ 53 | dispatchEvent(type, event) { 54 | if (!this._eventListeners[type]) { 55 | return 56 | } 57 | 58 | const listenersToCall = [] 59 | const listenersToCallAndRemove = [] 60 | this._eventListeners[type].forEach(({ listener, options }) => { 61 | if (options && options.once) { 62 | listenersToCallAndRemove.push(listener) 63 | } else { 64 | listenersToCall.push(listener) 65 | } 66 | }) 67 | 68 | listenersToCallAndRemove.forEach(listener => { 69 | this.removeEventListener(type, listener) 70 | listener(event) 71 | }) 72 | listenersToCall.forEach(listener => { 73 | listener(event) 74 | }) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/models/davObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import DAVEventListener from './davEventListener.js' 11 | import NetworkRequestClientError from '../errors/networkRequestClientError.js' 12 | import * as NS from '../utility/namespaceUtility.js' 13 | 14 | import { debugFactory } from '../debug.js' 15 | const debug = debugFactory('DavObject') 16 | 17 | /** 18 | * @class 19 | * @classdesc Generic DavObject aka file 20 | */ 21 | export class DavObject extends DAVEventListener { 22 | 23 | /** 24 | * @param {DavCollection} parent - The parent collection this DavObject is a child of 25 | * @param {Request} request - The request object initialized by DavClient 26 | * @param {string} url - Full url of this DavObject 27 | * @param {object} props - Properties including etag, content-type, etc. 28 | * @param {boolean} isPartial - Are we dealing with the complete or just partial addressbook / calendar data 29 | */ 30 | constructor(parent, request, url, props, isPartial = false) { 31 | super() 32 | 33 | Object.assign(this, { 34 | // parameters 35 | _parent: parent, 36 | _request: request, 37 | _url: url, 38 | _props: props, 39 | // housekeeping 40 | _isPartial: isPartial, 41 | _isDirty: false, 42 | }) 43 | 44 | this._exposeProperty('etag', NS.DAV, 'getetag', true) 45 | this._exposeProperty('contenttype', NS.DAV, 'getcontenttype') 46 | 47 | Object.defineProperty(this, 'url', { 48 | get: () => this._url, 49 | }) 50 | } 51 | 52 | /** 53 | * gets unfiltered data for this object 54 | * 55 | * @param {boolean} forceReFetch Always refetch data, even if not partial 56 | * @return {Promise} 57 | */ 58 | async fetchCompleteData(forceReFetch = false) { 59 | if (!forceReFetch && !this.isPartial()) { 60 | return 61 | } 62 | 63 | const request = await this._request.propFind(this._url, this.constructor.getPropFindList(), 0) 64 | this._props = request.body 65 | this._isDirty = false 66 | this._isPartial = false 67 | } 68 | 69 | /** 70 | * copies a DavObject to a different DavCollection 71 | * @param {DavCollection} collection 72 | * @param {boolean} overwrite 73 | * @param headers 74 | * @return {Promise} Promise that resolves to the copied DavObject 75 | */ 76 | async copy(collection, overwrite = false, headers = {}) { 77 | debug(`copying ${this.url} from ${this._parent.url} to ${collection.url}`) 78 | 79 | if (this._parent === collection) { 80 | throw new Error('Copying an object to the collection it\'s already part of is not supported') 81 | } 82 | if (!this._parent.isSameCollectionTypeAs(collection)) { 83 | throw new Error('Copying an object to a collection of a different type is not supported') 84 | } 85 | if (!collection.isWriteable()) { 86 | throw new Error('Can not copy object into read-only destination collection') 87 | } 88 | 89 | const uri = this.url.split('/').splice(-1, 1)[0] 90 | const destination = collection.url + uri 91 | 92 | await this._request.copy(this.url, destination, 0, overwrite, headers) 93 | return collection.find(uri) 94 | } 95 | 96 | /** 97 | * moves a DavObject to a different DavCollection 98 | * @param {DavCollection} collection 99 | * @param {boolean} overwrite 100 | * @param headers 101 | * @return {Promise} 102 | */ 103 | async move(collection, overwrite = false, headers = {}) { 104 | debug(`moving ${this.url} from ${this._parent.url} to ${collection.url}`) 105 | 106 | if (this._parent === collection) { 107 | throw new Error('Moving an object to the collection it\'s already part of is not supported') 108 | } 109 | if (!this._parent.isSameCollectionTypeAs(collection)) { 110 | throw new Error('Moving an object to a collection of a different type is not supported') 111 | } 112 | if (!collection.isWriteable()) { 113 | throw new Error('Can not move object into read-only destination collection') 114 | } 115 | 116 | const uri = this.url.split('/').splice(-1, 1)[0] 117 | const destination = collection.url + uri 118 | 119 | await this._request.move(this.url, destination, overwrite, headers) 120 | this._parent = collection 121 | this._url = destination 122 | } 123 | 124 | /** 125 | * updates the DavObject on the server 126 | * @return {Promise} 127 | */ 128 | async update() { 129 | // 1. Do not update filtered objects, because we would be loosing data on the server 130 | // 2. No need to update if object was never modified 131 | // 3. Do not update if called directly on DavObject, because there is no data prop 132 | if (this.isPartial() || !this.isDirty() || !this.data) { 133 | return 134 | } 135 | 136 | const headers = {} 137 | 138 | // updating an object should use it's content-type 139 | if (this.contenttype) { 140 | headers['Content-Type'] = `${this.contenttype}; charset=utf-8` 141 | } 142 | if (this.etag) { 143 | headers['If-Match'] = this.etag 144 | } 145 | 146 | return this._request.put(this.url, headers, this.data).then((res) => { 147 | this._isDirty = false 148 | // Don't overwrite content-type, it's set to text/html in the response ... 149 | this._props['{DAV:}getetag'] = res.headers.etag || null 150 | }).catch((ex) => { 151 | this._isDirty = true 152 | 153 | if (ex instanceof NetworkRequestClientError && ex.status === 412) { 154 | this._isPartial = true 155 | } 156 | 157 | throw ex 158 | }) 159 | } 160 | 161 | /** 162 | * deletes the DavObject on the server 163 | * 164 | * @param headers 165 | * @return {Promise<{body: string|object, status: number, headers: object}>} 166 | */ 167 | async delete(headers = {}) { 168 | return this._request.delete(this.url, headers) 169 | } 170 | 171 | /** 172 | * returns whether the data in this DavObject is the result of a partial retrieval 173 | * 174 | * @return {boolean} 175 | */ 176 | isPartial() { 177 | return this._isPartial 178 | } 179 | 180 | /** 181 | * returns whether the data in this DavObject contains unsynced changes 182 | * 183 | * @return {boolean} 184 | */ 185 | isDirty() { 186 | return this._isDirty 187 | } 188 | 189 | /** 190 | * @protected 191 | * @param {string} localName 192 | * @param {string} xmlNamespace 193 | * @param {string} xmlName 194 | * @param {boolean} mutable 195 | * @return void 196 | */ 197 | _exposeProperty(localName, xmlNamespace, xmlName, mutable = false) { 198 | if (mutable) { 199 | Object.defineProperty(this, localName, { 200 | get: () => this._props[`{${xmlNamespace}}${xmlName}`], 201 | set: (val) => { 202 | this._isDirty = true 203 | this._props[`{${xmlNamespace}}${xmlName}`] = val 204 | }, 205 | }) 206 | } else { 207 | Object.defineProperty(this, localName, { 208 | get: () => this._props[`{${xmlNamespace}}${xmlName}`], 209 | }) 210 | } 211 | } 212 | 213 | /** 214 | * A list of all property names that should be included 215 | * in propfind requests that may include this object 216 | * 217 | * @return {string[][]} 218 | */ 219 | static getPropFindList() { 220 | return [ 221 | [NS.DAV, 'getcontenttype'], 222 | [NS.DAV, 'getetag'], 223 | [NS.DAV, 'resourcetype'], 224 | ] 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /src/models/deletedCalendar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { Calendar } from './calendar.js' 11 | 12 | /** 13 | * This class represents a deleted calendar collection. 14 | * 15 | * @augments Calendar 16 | */ 17 | export class DeletedCalendar extends Calendar { 18 | } 19 | -------------------------------------------------------------------------------- /src/models/principal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { DavObject } from './davObject.js' 11 | import * as NS from '../utility/namespaceUtility.js' 12 | import * as XMLUtility from '../utility/xmlUtility.js' 13 | 14 | import prinicipalPropSet from '../propset/principalPropSet.js' 15 | 16 | /** 17 | * @typedef {object} PrincipalPropfindOptions 18 | * @property {boolean=} PrincipalPropfindOptions.enableCalDAV 19 | * @property {boolean=} PrincipalPropfindOptions.enableCalDAVResourceBooking 20 | * @property {boolean=} PrincipalPropfindOptions.enableCardDAV 21 | */ 22 | 23 | /** 24 | * @class 25 | */ 26 | export class Principal extends DavObject { 27 | 28 | /** 29 | * Creates an object that represents a single principal 30 | * as specified in RFC 3744 31 | * 32 | * https://tools.ietf.org/html/rfc3744#section-2 33 | * 34 | * @inheritDoc 35 | */ 36 | constructor(...args) { 37 | super(...args) 38 | 39 | Object.assign(this, { 40 | // house keeping 41 | _updatedProperties: [], 42 | 43 | // parsers / factories 44 | _propSetFactory: [], 45 | }) 46 | 47 | this._registerPropSetFactory(prinicipalPropSet) 48 | 49 | this._exposeProperty('displayname', NS.DAV, 'displayname') 50 | this._exposeProperty('calendarUserType', NS.IETF_CALDAV, 'calendar-user-type') 51 | this._exposeProperty('calendarUserAddressSet', NS.IETF_CALDAV, 'calendar-user-address-set') 52 | this._exposeProperty('principalUrl', NS.DAV, 'principal-URL') 53 | this._exposeProperty('email', NS.SABREDAV, 'email-address') 54 | this._exposeProperty('language', NS.NEXTCLOUD, 'language') 55 | 56 | this._exposeProperty('calendarHomes', NS.IETF_CALDAV, 'calendar-home-set') 57 | this._exposeProperty('scheduleInbox', NS.IETF_CALDAV, 'schedule-inbox-URL') 58 | this._exposeProperty('scheduleOutbox', NS.IETF_CALDAV, 'schedule-outbox-URL') 59 | this._exposeProperty('scheduleDefaultCalendarUrl', NS.IETF_CALDAV, 'schedule-default-calendar-URL', true) 60 | 61 | this._exposeProperty('addressBookHomes', NS.IETF_CARDDAV, 'addressbook-home-set') 62 | 63 | // Room and resource booking related 64 | this._exposeProperty('roomType', NS.NEXTCLOUD, 'room-type') 65 | this._exposeProperty('roomSeatingCapacity', NS.NEXTCLOUD, 'room-seating-capacity') 66 | this._exposeProperty('roomBuildingAddress', NS.NEXTCLOUD, 'room-building-address') 67 | this._exposeProperty('roomBuildingStory', NS.NEXTCLOUD, 'room-building-story') 68 | this._exposeProperty('roomBuildingRoomNumber', NS.NEXTCLOUD, 'room-building-room-number') 69 | this._exposeProperty('roomFeatures', NS.NEXTCLOUD, 'room-features') 70 | 71 | Object.defineProperties(this, { 72 | principalScheme: { 73 | get: () => { 74 | const baseUrl = this._request.pathname(this._request.baseUrl) 75 | let principalURI = this.url.slice(baseUrl.length) 76 | if (principalURI.slice(-1) === '/') { 77 | principalURI = principalURI.slice(0, -1) 78 | } 79 | 80 | return 'principal:' + principalURI 81 | }, 82 | }, 83 | userId: { 84 | get: () => { 85 | if (this.calendarUserType !== 'INDIVIDUAL') { 86 | return null 87 | } 88 | 89 | return this.url.split('/').splice(-2, 2)[this.url.endsWith('/') ? 0 : 1] 90 | }, 91 | }, 92 | groupId: { 93 | get: () => { 94 | if (this.calendarUserType !== 'GROUP') { 95 | return null 96 | } 97 | 98 | return this.url.split('/').splice(-2, 2)[this.url.endsWith('/') ? 0 : 1] 99 | }, 100 | }, 101 | resourceId: { 102 | get: () => { 103 | if (this.calendarUserType !== 'RESOURCE') { 104 | return null 105 | } 106 | 107 | return this.url.split('/').splice(-2, 2)[this.url.endsWith('/') ? 0 : 1] 108 | }, 109 | }, 110 | roomId: { 111 | get: () => { 112 | if (this.calendarUserType !== 'ROOM') { 113 | return null 114 | } 115 | 116 | return this.url.split('/').splice(-2, 2)[this.url.endsWith('/') ? 0 : 1] 117 | }, 118 | }, 119 | roomAddress: { 120 | get: () => { 121 | const data = [ 122 | this.roomBuildingRoomNumber, 123 | this.roomBuildingStory, 124 | this.roomBuildingAddress, 125 | ] 126 | return data 127 | .filter(value => !!value) 128 | .join(', ') 129 | }, 130 | }, 131 | }) 132 | } 133 | 134 | /** 135 | * Expose property to the outside and track changes if it's mutable 136 | * 137 | * @protected 138 | * @param {string} localName 139 | * @param {string} xmlNamespace 140 | * @param {string} xmlName 141 | * @param {boolean} mutable 142 | * @return void 143 | */ 144 | _exposeProperty(localName, xmlNamespace, xmlName, mutable = false) { 145 | if (mutable) { 146 | Object.defineProperty(this, localName, { 147 | get: () => this._props[`{${xmlNamespace}}${xmlName}`], 148 | set: (val) => { 149 | this._props[`{${xmlNamespace}}${xmlName}`] = val 150 | if (this._updatedProperties.indexOf(`{${xmlNamespace}}${xmlName}`) === -1) { 151 | this._updatedProperties.push(`{${xmlNamespace}}${xmlName}`) 152 | } 153 | }, 154 | }) 155 | } else { 156 | Object.defineProperty(this, localName, { 157 | get: () => this._props[`{${xmlNamespace}}${xmlName}`], 158 | }) 159 | } 160 | } 161 | 162 | /** 163 | * @protected 164 | * @param factory 165 | * @return void 166 | */ 167 | _registerPropSetFactory(factory) { 168 | this._propSetFactory.push(factory) 169 | } 170 | 171 | /** 172 | * @inheritDoc 173 | * 174 | * @param {PrincipalPropfindOptions} options 175 | */ 176 | static getPropFindList(options = {}) { 177 | const list = [ 178 | [NS.DAV, 'displayname'], 179 | [NS.IETF_CALDAV, 'calendar-user-type'], 180 | [NS.IETF_CALDAV, 'calendar-user-address-set'], 181 | [NS.DAV, 'principal-URL'], 182 | [NS.DAV, 'alternate-URI-set'], 183 | [NS.SABREDAV, 'email-address'], 184 | [NS.NEXTCLOUD, 'language'], 185 | ] 186 | 187 | if (options.enableCalDAV) { 188 | list.push( 189 | [NS.IETF_CALDAV, 'calendar-home-set'], 190 | [NS.IETF_CALDAV, 'schedule-inbox-URL'], 191 | [NS.IETF_CALDAV, 'schedule-outbox-URL'], 192 | [NS.IETF_CALDAV, 'schedule-default-calendar-URL'], 193 | ) 194 | } 195 | if (options.enableCalDAVResourceBooking || options.enableCalDAV) { 196 | list.push( 197 | // Room and Resource booking related 198 | [NS.NEXTCLOUD, 'resource-type'], 199 | [NS.NEXTCLOUD, 'resource-vehicle-type'], 200 | [NS.NEXTCLOUD, 'resource-vehicle-make'], 201 | [NS.NEXTCLOUD, 'resource-vehicle-model'], 202 | [NS.NEXTCLOUD, 'resource-vehicle-is-electric'], 203 | [NS.NEXTCLOUD, 'resource-vehicle-range'], 204 | [NS.NEXTCLOUD, 'resource-vehicle-seating-capacity'], 205 | [NS.NEXTCLOUD, 'resource-contact-person'], 206 | [NS.NEXTCLOUD, 'resource-contact-person-vcard'], 207 | [NS.NEXTCLOUD, 'room-type'], 208 | [NS.NEXTCLOUD, 'room-seating-capacity'], 209 | [NS.NEXTCLOUD, 'room-building-address'], 210 | [NS.NEXTCLOUD, 'room-building-story'], 211 | [NS.NEXTCLOUD, 'room-building-room-number'], 212 | [NS.NEXTCLOUD, 'room-features'], 213 | ) 214 | } 215 | if (options.enableCardDAV) { 216 | list.push( 217 | [NS.IETF_CARDDAV, 'addressbook-home-set'], 218 | ) 219 | } 220 | 221 | return list 222 | } 223 | 224 | /** 225 | * Sends a PropPatch request to update the principal's properties. 226 | * The request is only made if properties actually changed. 227 | * 228 | * @return {Promise} 229 | */ 230 | async update() { 231 | if (this._updatedProperties.length === 0) { 232 | return 233 | } 234 | 235 | const properties = {} 236 | this._updatedProperties.forEach((updatedProperty) => { 237 | properties[updatedProperty] = this._props[updatedProperty] 238 | }) 239 | const propSet = this._propSetFactory.reduce((arr, p) => [...arr, ...p(properties)], []) 240 | 241 | const [skeleton, dPropSet] = XMLUtility.getRootSkeleton( 242 | [NS.DAV, 'propertyupdate'], 243 | [NS.DAV, 'set'], 244 | [NS.DAV, 'prop']) 245 | 246 | dPropSet.push(...propSet) 247 | 248 | const body = XMLUtility.serialize(skeleton) 249 | await this._request.propPatch(this._url, {}, body) 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /src/models/scheduleInbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { Calendar } from './calendar.js' 11 | import * as NS from '../utility/namespaceUtility.js' 12 | import scheduleInboxPropSet from '../propset/scheduleInboxPropSet.js' 13 | 14 | export default class ScheduleInbox extends Calendar { 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | constructor(...args) { 20 | super(...args) 21 | 22 | super._registerPropSetFactory(scheduleInboxPropSet) 23 | 24 | // https://tools.ietf.org/html/rfc7953#section-7.2.4 25 | super._exposeProperty('availability', NS.IETF_CALDAV, 'calendar-availability', true) 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | static getPropFindList() { 32 | return super.getPropFindList().concat([ 33 | [NS.IETF_CALDAV, 'calendar-availability'], 34 | ]) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/models/scheduleOutbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { DavCollection } from './davCollection.js' 11 | import * as NS from '../utility/namespaceUtility.js' 12 | 13 | export default class ScheduleOutbox extends DavCollection { 14 | 15 | /** 16 | * Sends a free-busy-request for the scheduling outbox 17 | * The data is required to be a valid iTIP data. 18 | * For an example, see https://tools.ietf.org/html/rfc6638#appendix-B.5 19 | * 20 | * @param {string} data iTIP with VFREEBUSY component and METHOD:REQUEST 21 | * @return {Promise} 22 | */ 23 | async freeBusyRequest(data) { 24 | const result = {} 25 | const response = await this._request.post(this.url, { 26 | 'Content-Type': 'text/calendar; charset="utf-8"', 27 | }, data) 28 | 29 | const domParser = new DOMParser() 30 | const document = domParser.parseFromString(response.body, 'application/xml') 31 | 32 | const responses = document.evaluate('/cl:schedule-response/cl:response', document, NS.resolve, XPathResult.ANY_TYPE, null) 33 | let responseNode 34 | 35 | while ((responseNode = responses.iterateNext()) !== null) { 36 | const recipient = document.evaluate('string(cl:recipient/d:href)', responseNode, NS.resolve, XPathResult.ANY_TYPE, null).stringValue 37 | const status = document.evaluate('string(cl:request-status)', responseNode, NS.resolve, XPathResult.ANY_TYPE, null).stringValue 38 | const calendarData = document.evaluate('string(cl:calendar-data)', responseNode, NS.resolve, XPathResult.ANY_TYPE, null).stringValue 39 | const success = /^2.\d(;.+)?$/.test(status) 40 | 41 | result[recipient] = { 42 | calendarData, 43 | status, 44 | success, 45 | } 46 | } 47 | 48 | return result 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/models/subscription.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { Calendar } from './calendar.js' 11 | import * as NS from '../utility/namespaceUtility.js' 12 | 13 | /** 14 | * This class represents a subscription collection 15 | * It is being cached on the Nextcloud server and allows to be queried by standard CalDAV requests. 16 | * 17 | * On top of that it contains more non-standard apple properties 18 | */ 19 | export class Subscription extends Calendar { 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | constructor(...args) { 25 | super(...args) 26 | 27 | super._exposeProperty('source', NS.CALENDARSERVER, 'source', true) 28 | super._exposeProperty('refreshRate', NS.APPLE, 'refreshrate', true) 29 | super._exposeProperty('stripTodos', NS.CALENDARSERVER, 'subscribed-strip-todos', true) 30 | super._exposeProperty('stripAlarms', NS.CALENDARSERVER, 'subscribed-strip-alarms', true) 31 | super._exposeProperty('stripAttachments', NS.CALENDARSERVER, 'subscribed-strip-attachments', true) 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | static getPropFindList() { 38 | return super.getPropFindList().concat([ 39 | [NS.CALENDARSERVER, 'source'], 40 | [NS.APPLE, 'refreshrate'], 41 | [NS.CALENDARSERVER, 'subscribed-strip-todos'], 42 | [NS.CALENDARSERVER, 'subscribed-strip-alarms'], 43 | [NS.CALENDARSERVER, 'subscribed-strip-attachments'], 44 | ]) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/models/vcard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { DavObject } from './davObject.js' 11 | import * as NS from '../utility/namespaceUtility.js' 12 | 13 | /** 14 | * @class 15 | */ 16 | export class VCard extends DavObject { 17 | 18 | /** 19 | * Creates a VCard that is supposed to store address-data 20 | * as specified in RFC 6350. 21 | * 22 | * https://tools.ietf.org/html/rfc6350 23 | * 24 | * @inheritDoc 25 | */ 26 | constructor(...args) { 27 | super(...args) 28 | 29 | super._exposeProperty('data', NS.IETF_CARDDAV, 'address-data', true) 30 | super._exposeProperty('hasphoto', NS.NEXTCLOUD, 'has-photo', false) 31 | } 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | static getPropFindList() { 37 | return super.getPropFindList().concat([ 38 | [NS.IETF_CARDDAV, 'address-data'], 39 | ]) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/models/vobject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { DavObject } from './davObject.js' 11 | import * as NS from '../utility/namespaceUtility.js' 12 | 13 | /** 14 | * @class 15 | */ 16 | export class VObject extends DavObject { 17 | 18 | /** 19 | * Creates a VObject that is supposed to store calendar-data 20 | * as specified in RFC 5545. 21 | * 22 | * https://tools.ietf.org/html/rfc5545 23 | * 24 | * @inheritDoc 25 | */ 26 | constructor(...args) { 27 | super(...args) 28 | 29 | super._exposeProperty('data', NS.IETF_CALDAV, 'calendar-data', true) 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | static getPropFindList() { 36 | return super.getPropFindList().concat([ 37 | [NS.IETF_CALDAV, 'calendar-data'], 38 | ]) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/propset/addressBookPropSet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import * as NS from '../utility/namespaceUtility.js' 11 | 12 | /** 13 | * 14 | * This function is capable of creating the propset xml structure for: 15 | * - {urn:ietf:params:xml:ns:carddav}addressbook-description 16 | * - {http://owncloud.org/ns}enabled 17 | * 18 | * @param {object} props 19 | * @return {object} 20 | */ 21 | export default function addressBookPropSet(props) { 22 | const xmlified = [] 23 | 24 | Object.entries(props).forEach(([key, value]) => { 25 | switch (key) { 26 | case '{urn:ietf:params:xml:ns:carddav}addressbook-description': 27 | xmlified.push({ 28 | name: [NS.IETF_CARDDAV, 'addressbook-description'], 29 | value, 30 | }) 31 | break 32 | 33 | case '{http://owncloud.org/ns}enabled': 34 | xmlified.push({ 35 | name: [NS.OWNCLOUD, 'enabled'], 36 | value: value ? '1' : '0', 37 | }) 38 | break 39 | 40 | default: 41 | break 42 | } 43 | }) 44 | 45 | return xmlified 46 | } 47 | -------------------------------------------------------------------------------- /src/propset/calendarPropSet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import * as NS from '../utility/namespaceUtility.js' 11 | 12 | /** 13 | * 14 | * This function is capable of creating the propset xml structure for: 15 | * - {http://apple.com/ns/ical/}calendar-order 16 | * - {http://apple.com/ns/ical/}calendar-color 17 | * - {http://calendarserver.org/ns/}source 18 | * - {urn:ietf:params:xml:ns:caldav}calendar-description 19 | * - {urn:ietf:params:xml:ns:caldav}calendar-timezone 20 | * - {http://owncloud.org/ns}calendar-enabled 21 | * 22 | * @param {object} props 23 | * @return {object} 24 | */ 25 | export default function calendarPropSet(props) { 26 | const xmlified = [] 27 | 28 | Object.entries(props).forEach(([key, value]) => { 29 | switch (key) { 30 | case '{http://apple.com/ns/ical/}calendar-order': 31 | xmlified.push({ 32 | name: [NS.APPLE, 'calendar-order'], 33 | value: value.toString(), 34 | }) 35 | break 36 | 37 | case '{http://apple.com/ns/ical/}calendar-color': 38 | xmlified.push({ 39 | name: [NS.APPLE, 'calendar-color'], 40 | value, 41 | }) 42 | break 43 | 44 | case '{http://calendarserver.org/ns/}source': 45 | xmlified.push({ 46 | name: [NS.CALENDARSERVER, 'source'], 47 | children: [{ 48 | name: [NS.DAV, 'href'], 49 | value, 50 | }], 51 | }) 52 | break 53 | 54 | case '{urn:ietf:params:xml:ns:caldav}calendar-description': 55 | xmlified.push({ 56 | name: [NS.IETF_CALDAV, 'calendar-description'], 57 | value, 58 | }) 59 | break 60 | 61 | case '{urn:ietf:params:xml:ns:caldav}calendar-timezone': 62 | xmlified.push({ 63 | name: [NS.IETF_CALDAV, 'calendar-timezone'], 64 | value, 65 | }) 66 | break 67 | 68 | case '{http://owncloud.org/ns}calendar-enabled': 69 | xmlified.push({ 70 | name: [NS.OWNCLOUD, 'calendar-enabled'], 71 | value: value ? '1' : '0', 72 | }) 73 | break 74 | case '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': 75 | xmlified.push({ 76 | name: [NS.IETF_CALDAV, 'schedule-calendar-transp'], 77 | children: [{ 78 | name: [NS.IETF_CALDAV, value], 79 | }], 80 | }) 81 | break 82 | default: 83 | break 84 | } 85 | }) 86 | 87 | return xmlified 88 | } 89 | -------------------------------------------------------------------------------- /src/propset/davCollectionPropSet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import * as NS from '../utility/namespaceUtility.js' 11 | 12 | /** 13 | * 14 | * This function is capable of creating the propset xml structure for: 15 | * - {DAV:}displayname 16 | * 17 | * @param {object} props 18 | * @return {object} 19 | */ 20 | export default function davCollectionPropSet(props) { 21 | const xmlified = [] 22 | 23 | Object.entries(props).forEach(([key, value]) => { 24 | switch (key) { 25 | case '{DAV:}displayname': 26 | xmlified.push({ 27 | name: [NS.DAV, 'displayname'], 28 | value, 29 | }) 30 | break 31 | 32 | default: 33 | break 34 | } 35 | }) 36 | 37 | return xmlified 38 | } 39 | -------------------------------------------------------------------------------- /src/propset/principalPropSet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import * as NS from '../utility/namespaceUtility.js' 11 | 12 | /** 13 | * This function is capable of creating the propset xml structure for: 14 | * - '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL': 15 | * 16 | * @param {object} props 17 | * @return {object} 18 | */ 19 | export default function prinicipalPropSet(props) { 20 | const xmlified = [] 21 | 22 | Object.entries(props).forEach(([key, value]) => { 23 | switch (key) { 24 | case '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL': 25 | xmlified.push({ 26 | name: [NS.IETF_CALDAV, 'schedule-default-calendar-URL'], 27 | children: [ 28 | { 29 | name: ['DAV:', 'href'], 30 | value, 31 | }, 32 | ], 33 | }) 34 | break 35 | } 36 | }) 37 | 38 | return xmlified 39 | } 40 | -------------------------------------------------------------------------------- /src/propset/scheduleInboxPropSet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import * as NS from '../utility/namespaceUtility.js' 11 | 12 | /** 13 | * This function is capable of creating the propset xml structure for: 14 | * - {urn:ietf:params:xml:ns:caldav}calendar-availability 15 | * 16 | * @param {object} props 17 | * @return {object} 18 | */ 19 | export default function calendarPropSet(props) { 20 | const xmlified = [] 21 | 22 | Object.entries(props).forEach(([key, value]) => { 23 | switch (key) { 24 | case '{urn:ietf:params:xml:ns:caldav}calendar-availability': 25 | xmlified.push({ 26 | name: [NS.IETF_CALDAV, 'calendar-availability'], 27 | value: value.toString(), 28 | }) 29 | break 30 | } 31 | }) 32 | 33 | return xmlified 34 | } 35 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import * as NS from './utility/namespaceUtility.js' 11 | import * as XMLUtility from './utility/xmlUtility.js' 12 | import axios from '@nextcloud/axios' 13 | 14 | import NetworkRequestAbortedError from './errors/networkRequestAbortedError.js' 15 | import NetworkRequestError from './errors/networkRequestError.js' 16 | import NetworkRequestServerError from './errors/networkRequestServerError.js' 17 | import NetworkRequestClientError from './errors/networkRequestClientError.js' 18 | import NetworkRequestHttpError from './errors/networkRequestHttpError.js' 19 | 20 | /** 21 | * Request class is used to send any kind of request to the DAV server 22 | * It also parses incoming XML responses 23 | */ 24 | export default class Request { 25 | 26 | /** 27 | * Creates a new Request object 28 | * 29 | * @param {string} baseUrl - root url of DAV server, use OC.remote('dav') 30 | * @param {{[name: string]: any}} [defaultHeaders] - additional HTTP headers to send with each request 31 | * @param {import('./parser.js').Parser} parser - instance of Parser class 32 | */ 33 | constructor(baseUrl, parser, defaultHeaders = {}) { 34 | this.baseUrl = baseUrl 35 | this.parser = parser 36 | this.defaultHeaders = defaultHeaders 37 | } 38 | 39 | /** 40 | * sends an OPTIONS request 41 | * 42 | * @param {string} url - URL to do the request on 43 | * @param {object} headers - additional HTTP headers to send 44 | * @param {AbortSignal} abortSignal - the signal from an abort controller 45 | * @return {Promise<{body: string|object, status: number, headers: object}>} 46 | */ 47 | async options(url, headers = {}, abortSignal = null) { 48 | return this.request('OPTIONS', url, headers, null, abortSignal) 49 | } 50 | 51 | /** 52 | * sends a GET request 53 | * 54 | * @param {string} url - URL to do the request on 55 | * @param {object} headers - additional HTTP headers to send 56 | * @param {string} body - request body 57 | * @param {AbortSignal} abortSignal - the signal from an abort controller 58 | * @return {Promise<{body: string|object, status: number, headers: object}>} 59 | */ 60 | async get(url, headers = {}, body = null, abortSignal = null) { 61 | return this.request('GET', url, headers, body, abortSignal) 62 | } 63 | 64 | /** 65 | * sends a PATCH request 66 | * 67 | * @param {string} url - URL to do the request on 68 | * @param {object} headers - additional HTTP headers to send 69 | * @param {string} body - request body 70 | * @param {AbortSignal} abortSignal - the signal from an abort controller 71 | * @return {Promise<{body: string|object, status: number, headers: object}>} 72 | */ 73 | async patch(url, headers, body, abortSignal = null) { 74 | return this.request('PATCH', url, headers, body, abortSignal) 75 | } 76 | 77 | /** 78 | * sends a POST request 79 | * 80 | * @param {string} url - URL to do the request on 81 | * @param {object} headers - additional HTTP headers to send 82 | * @param {string} body - request body 83 | * @param {AbortSignal} abortSignal - the signal from an abort controller 84 | * @return {Promise<{body: string|object, status: number, headers: object}>} 85 | */ 86 | async post(url, headers, body, abortSignal = null) { 87 | return this.request('POST', url, headers, body, abortSignal) 88 | } 89 | 90 | /** 91 | * sends a PUT request 92 | * 93 | * @param {string} url - URL to do the request on 94 | * @param {object} headers - additional HTTP headers to send 95 | * @param {string} body - request body 96 | * @param {AbortSignal} abortSignal - the signal from an abort controller 97 | * @return {Promise<{body: string|object, status: number, headers: object}>} 98 | */ 99 | async put(url, headers, body, abortSignal = null) { 100 | return this.request('PUT', url, headers, body, abortSignal) 101 | } 102 | 103 | /** 104 | * sends a DELETE request 105 | * 106 | * @param {string} url - URL to do the request on 107 | * @param {object} headers - additional HTTP headers to send 108 | * @param {string} body - request body 109 | * @param {AbortSignal} abortSignal - the signal from an abort controller 110 | * @return {Promise<{body: string|object, status: number, headers: object}>} 111 | */ 112 | async delete(url, headers = {}, body = null, abortSignal = null) { 113 | return this.request('DELETE', url, headers, body, abortSignal) 114 | } 115 | 116 | /** 117 | * sends a COPY request 118 | * https://tools.ietf.org/html/rfc4918#section-9.8 119 | * 120 | * @param {string} url - URL to do the request on 121 | * @param {string} destination - place to copy the object/collection to 122 | * @param {number | string} depth - 0 = copy collection without content, Infinity = copy collection with content 123 | * @param {boolean} overwrite - whether or not to overwrite destination if existing 124 | * @param {object} headers - additional HTTP headers to send 125 | * @param {string} body - request body 126 | * @param {AbortSignal} abortSignal - the signal from an abort controller 127 | * @return {Promise<{body: string|object, status: number, headers: object}>} 128 | */ 129 | async copy(url, destination, depth = 0, overwrite = false, headers = {}, body = null, abortSignal = null) { 130 | headers.Destination = destination 131 | headers.Depth = depth 132 | headers.Overwrite = overwrite ? 'T' : 'F' 133 | 134 | return this.request('COPY', url, headers, body, abortSignal) 135 | } 136 | 137 | /** 138 | * sends a MOVE request 139 | * https://tools.ietf.org/html/rfc4918#section-9.9 140 | * 141 | * @param {string} url - URL to do the request on 142 | * @param {string} destination - place to move the object/collection to 143 | * @param {boolean} overwrite - whether or not to overwrite destination if existing 144 | * @param {object} headers - additional HTTP headers to send 145 | * @param {string} body - request body 146 | * @param {AbortSignal} abortSignal - the signal from an abort controller 147 | * @return {Promise<{body: string|object, status: number, headers: object}>} 148 | */ 149 | async move(url, destination, overwrite = false, headers = {}, body = null, abortSignal = null) { 150 | headers.Destination = destination 151 | headers.Depth = 'Infinity' 152 | headers.Overwrite = overwrite ? 'T' : 'F' 153 | 154 | return this.request('MOVE', url, headers, body, abortSignal) 155 | } 156 | 157 | /** 158 | * sends a LOCK request 159 | * https://tools.ietf.org/html/rfc4918#section-9.10 160 | * 161 | * @param {string} url - URL to do the request on 162 | * @param {object} headers - additional HTTP headers to send 163 | * @param {string} body - request body 164 | * @param {AbortSignal} abortSignal - the signal from an abort controller 165 | * @return {Promise<{body: string|object, status: number, headers: object}>} 166 | */ 167 | async lock(url, headers = {}, body = null, abortSignal = null) { 168 | 169 | // TODO - add parameters for Depth and Timeout 170 | 171 | return this.request('LOCK', url, headers, body, abortSignal) 172 | } 173 | 174 | /** 175 | * sends an UNLOCK request 176 | * https://tools.ietf.org/html/rfc4918#section-9.11 177 | * 178 | * @param {string} url - URL to do the request on 179 | * @param {object} headers - additional HTTP headers to send 180 | * @param {string} body - request body 181 | * @param {AbortSignal} abortSignal - the signal from an abort controller 182 | * @return {Promise<{body: string|object, status: number, headers: object}>} 183 | */ 184 | async unlock(url, headers = {}, body = null, abortSignal = null) { 185 | 186 | // TODO - add parameter for Lock-Token 187 | 188 | return this.request('UNLOCK', url, headers, body, abortSignal) 189 | } 190 | 191 | /** 192 | * sends a PROPFIND request 193 | * https://tools.ietf.org/html/rfc4918#section-9.1 194 | * 195 | * @param {string} url - URL to do the request on 196 | * @param {string[][]} properties - list of properties to search for, formatted as [namespace, localName] 197 | * @param {number | string} depth - Depth header to send 198 | * @param {object} headers - additional HTTP headers to send 199 | * @param {AbortSignal} abortSignal - the signal from an abort controller 200 | * @return {Promise<{body: string|object, status: number, headers: object}>} 201 | */ 202 | async propFind(url, properties, depth = 0, headers = {}, abortSignal = null) { 203 | // adjust headers 204 | headers.Depth = depth 205 | 206 | // create request body 207 | const [skeleton, dPropChildren] = XMLUtility.getRootSkeleton([NS.DAV, 'propfind'], [NS.DAV, 'prop']) 208 | dPropChildren.push(...properties.map(p => ({ name: p }))) 209 | const body = XMLUtility.serialize(skeleton) 210 | 211 | return this.request('PROPFIND', url, headers, body, abortSignal) 212 | } 213 | 214 | /** 215 | * sends a PROPPATCH request 216 | * https://tools.ietf.org/html/rfc4918#section-9.2 217 | * 218 | * @param {string} url - URL to do the request on 219 | * @param {object} headers - additional HTTP headers to send 220 | * @param {string} body - request body 221 | * @param {AbortSignal} abortSignal - the signal from an abort controller 222 | * @return {Promise<{body: string|object, status: number, headers: object}>} 223 | */ 224 | async propPatch(url, headers, body, abortSignal = null) { 225 | return this.request('PROPPATCH', url, headers, body, abortSignal) 226 | } 227 | 228 | /** 229 | * sends a MKCOL request 230 | * https://tools.ietf.org/html/rfc4918#section-9.3 231 | * https://tools.ietf.org/html/rfc5689 232 | * 233 | * @param {string} url - URL to do the request on 234 | * @param {object} headers - additional HTTP headers to send 235 | * @param {string} body - request body 236 | * @param {AbortSignal} abortSignal - the signal from an abort controller 237 | * @return {Promise<{body: string|object, status: number, headers: object}>} 238 | */ 239 | async mkCol(url, headers, body, abortSignal = null) { 240 | return this.request('MKCOL', url, headers, body, abortSignal) 241 | } 242 | 243 | /** 244 | * sends a REPORT request 245 | * https://tools.ietf.org/html/rfc3253#section-3.6 246 | * 247 | * @param {string} url - URL to do the request on 248 | * @param {object} headers - additional HTTP headers to send 249 | * @param {string} body - request body 250 | * @param {AbortSignal} abortSignal - the signal from an abort controller 251 | * @return {Promise<{body: string|object, status: number, headers: object}>} 252 | */ 253 | async report(url, headers, body, abortSignal = null) { 254 | return this.request('REPORT', url, headers, body, abortSignal) 255 | } 256 | 257 | /** 258 | * sends generic request 259 | * 260 | * @param {string} method - HTTP Method name 261 | * @param {string} url - URL to do the request on 262 | * @param {object} headers - additional HTTP headers to send 263 | * @param {string} body - request body 264 | * @param {AbortSignal} abortSignal - the signal from an abort controller 265 | * @return {Promise<{body: string|object, status: number, headers: object}>} 266 | */ 267 | async request(method, url, headers, body, abortSignal) { 268 | const assignHeaders = Object.assign({}, getDefaultHeaders(), this.defaultHeaders, headers) 269 | try { 270 | const response = await axios.request({ 271 | url: this.absoluteUrl(url), 272 | method, 273 | headers: assignHeaders, 274 | data: body, 275 | // all statuses not in success are treated as errors in catch 276 | validateStatus: wasRequestSuccessful, 277 | signal: abortSignal, 278 | }) 279 | 280 | let responseBody = response.data 281 | if (response.status === 207) { 282 | responseBody = this._parseMultiStatusResponse(responseBody) 283 | if (parseInt(assignHeaders.Depth, 10) === 0 && method === 'PROPFIND') { 284 | responseBody = responseBody[Object.keys(responseBody)[0]] 285 | } 286 | } 287 | 288 | return { 289 | body: responseBody, 290 | status: response.status, 291 | headers: response.headers, 292 | } 293 | } catch (error) { 294 | if (axios.isCancel(error)) { 295 | // xhr.onabort 296 | // AbortController.abort 297 | throw new NetworkRequestAbortedError({ 298 | body: null, 299 | status: -1, 300 | headers: error.headers || {}, 301 | }) 302 | } 303 | 304 | if (error.request) { 305 | // xhr.onerror 306 | throw new NetworkRequestError({ 307 | body: null, 308 | status: -1, 309 | headers: error.headers || {}, 310 | }) 311 | } 312 | 313 | if (error.status >= 400 && error.status < 500) { 314 | throw new NetworkRequestClientError({ 315 | body: error.data, 316 | status: error.status, 317 | headers: error.headers || {}, 318 | }) 319 | } 320 | if (error.status >= 500 && error.status < 600) { 321 | throw new NetworkRequestServerError({ 322 | body: error.data, 323 | status: error.status, 324 | headers: error.headers || {}, 325 | }) 326 | } 327 | 328 | throw new NetworkRequestHttpError({ 329 | body: error.data, 330 | status: error.status, 331 | headers: error.headers || {}, 332 | }) 333 | } 334 | } 335 | 336 | /** 337 | * returns name of file / folder of a url 338 | * 339 | * @params {string} url 340 | * @return {string} 341 | */ 342 | filename(url) { 343 | let pathname = this.pathname(url) 344 | if (pathname.slice(-1) === '/') { 345 | pathname = pathname.slice(0, -1) 346 | } 347 | 348 | const slashPos = pathname.lastIndexOf('/') 349 | return pathname.slice(slashPos) 350 | } 351 | 352 | /** 353 | * returns pathname for a URL 354 | * 355 | * @params {string} url 356 | * @return {string} 357 | */ 358 | pathname(url) { 359 | const urlObject = new URL(url, this.baseUrl) 360 | return urlObject.pathname 361 | } 362 | 363 | /** 364 | * returns absolute url 365 | * 366 | * @param {string} url 367 | * @return {string} 368 | */ 369 | absoluteUrl(url) { 370 | const urlObject = new URL(url, this.baseUrl) 371 | return urlObject.href 372 | } 373 | 374 | /** 375 | * parses a multi status response (207), sorts them by path 376 | * and drops all unsuccessful responses 377 | * 378 | * @param {string} body 379 | * @return {object} 380 | * @private 381 | */ 382 | _parseMultiStatusResponse(body) { 383 | const result = {} 384 | const domParser = new DOMParser() 385 | const document = domParser.parseFromString(body, 'application/xml') 386 | 387 | const responses = document.evaluate('/d:multistatus/d:response', document, NS.resolve, XPathResult.ANY_TYPE, null) 388 | let responseNode 389 | 390 | while ((responseNode = responses.iterateNext()) !== null) { 391 | const href = document.evaluate('string(d:href)', responseNode, NS.resolve, XPathResult.ANY_TYPE, null).stringValue 392 | const parsedProperties = {} 393 | const propStats = document.evaluate('d:propstat', responseNode, NS.resolve, XPathResult.ANY_TYPE, null) 394 | let propStatNode 395 | 396 | while ((propStatNode = propStats.iterateNext()) !== null) { 397 | const status = document.evaluate('string(d:status)', propStatNode, NS.resolve, XPathResult.ANY_TYPE, null).stringValue 398 | if (!wasRequestSuccessful(getStatusCodeFromString(status))) { 399 | continue 400 | } 401 | 402 | const props = document.evaluate('d:prop/*', propStatNode, NS.resolve, XPathResult.ANY_TYPE, null) 403 | let propNode 404 | 405 | while ((propNode = props.iterateNext()) !== null) { 406 | if (this.parser.canParse(`{${propNode.namespaceURI}}${propNode.localName}`)) { 407 | parsedProperties[`{${propNode.namespaceURI}}${propNode.localName}`] 408 | = this.parser.parse(document, propNode, NS.resolve) 409 | } 410 | } 411 | } 412 | 413 | result[href] = parsedProperties 414 | } 415 | 416 | return result 417 | } 418 | 419 | } 420 | 421 | /** 422 | * Check if response code is in the 2xx section 423 | * 424 | * @param {number} status 425 | * @return {boolean} 426 | * @private 427 | */ 428 | function wasRequestSuccessful(status) { 429 | return status >= 200 && status < 300 430 | } 431 | 432 | /** 433 | * Extract numeric status code from string like "HTTP/1.1 200 OK" 434 | * 435 | * @param {string} status 436 | * @return {number} 437 | * @private 438 | */ 439 | function getStatusCodeFromString(status) { 440 | return parseInt(status.split(' ')[1], 10) 441 | } 442 | 443 | /** 444 | * get object with default headers to include in every request 445 | * 446 | * @return {object} 447 | * @property {string} depth 448 | * @property {string} Content-Type 449 | * @private 450 | */ 451 | function getDefaultHeaders() { 452 | // TODO: https://tools.ietf.org/html/rfc4918#section-9.1 453 | // "Servers SHOULD treat request without a Depth header 454 | // as if a "Depth: infinity" header was included." 455 | // Should infinity be the default? 456 | 457 | return { 458 | Depth: '0', 459 | 'Content-Type': 'application/xml; charset=utf-8', 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /src/utility/namespaceUtility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | export const DAV = 'DAV:' 11 | export const IETF_CALDAV = 'urn:ietf:params:xml:ns:caldav' 12 | export const IETF_CARDDAV = 'urn:ietf:params:xml:ns:carddav' 13 | export const OWNCLOUD = 'http://owncloud.org/ns' 14 | export const NEXTCLOUD = 'http://nextcloud.com/ns' 15 | export const APPLE = 'http://apple.com/ns/ical/' 16 | export const CALENDARSERVER = 'http://calendarserver.org/ns/' 17 | export const SABREDAV = 'http://sabredav.org/ns' 18 | 19 | export const NS_MAP = { 20 | d: DAV, 21 | cl: IETF_CALDAV, 22 | cr: IETF_CARDDAV, 23 | oc: OWNCLOUD, 24 | nc: NEXTCLOUD, 25 | aapl: APPLE, 26 | cs: CALENDARSERVER, 27 | sd: SABREDAV, 28 | } 29 | 30 | /** 31 | * maps namespace like DAV: to it's short equivalent 32 | * 33 | * @param {string} short 34 | * @return {string} 35 | */ 36 | export function resolve(short) { 37 | return NS_MAP[short] || null 38 | } 39 | -------------------------------------------------------------------------------- /src/utility/stringUtility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | // uuidv4 taken from https://stackoverflow.com/a/2117523 11 | function uuidv4() { 12 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 13 | const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8) 14 | return v.toString(16).toUpperCase() 15 | }) 16 | } 17 | 18 | /** 19 | * generates a unique id with the option to pass a prefix and a filetype 20 | * 21 | * @param {string} prefix 22 | * @param {string} suffix 23 | * @return {string} 24 | */ 25 | export function uid(prefix, suffix) { 26 | prefix = prefix || '' 27 | suffix = suffix || '' 28 | 29 | if (prefix !== '') { 30 | prefix += '-' 31 | } 32 | if (suffix !== '') { 33 | suffix = '.' + suffix 34 | } 35 | 36 | return prefix + uuidv4() + suffix 37 | } 38 | 39 | /** 40 | * generates a uri and checks with isAvailable, whether or not the uri is still available 41 | * 42 | * @param {string} start 43 | * @param {Function} isAvailable 44 | * @return {string} 45 | */ 46 | export function uri(start, isAvailable) { 47 | start = start || '' 48 | 49 | let uri = start.toString().toLowerCase() 50 | .replace(/\s+/g, '-') // Replace spaces with - 51 | .replace(/[^\w-]+/g, '') // Remove all non-word chars 52 | .replace(/--+/g, '-') // Replace multiple - with single - 53 | .replace(/^-+/, '') // Trim - from start of text 54 | .replace(/-+$/, '') // Trim - from end of text 55 | 56 | if (uri === '') { 57 | uri = '-' 58 | } 59 | 60 | if (isAvailable(uri)) { 61 | return uri 62 | } 63 | 64 | if (uri.indexOf('-') === -1) { 65 | uri = uri + '-1' 66 | if (isAvailable(uri)) { 67 | return uri 68 | } 69 | } 70 | 71 | // === false because !undefined = true, possible infinite loop 72 | do { 73 | const positionLastDash = uri.lastIndexOf('-') 74 | const firstPart = uri.slice(0, positionLastDash) 75 | let lastPart = uri.slice(positionLastDash + 1) 76 | 77 | if (lastPart.match(/^\d+$/)) { 78 | lastPart = parseInt(lastPart) 79 | lastPart++ 80 | 81 | uri = firstPart + '-' + lastPart 82 | } else { 83 | uri = uri + '-1' 84 | } 85 | } while (isAvailable(uri) === false) 86 | 87 | return uri 88 | } 89 | -------------------------------------------------------------------------------- /src/utility/xmlUtility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | const serializer = new XMLSerializer() 11 | let prefixMap = {} 12 | 13 | /** 14 | * builds the root skeleton 15 | * 16 | * @params {...Array} array of namespace / name pairs 17 | * @return {*[]} 18 | */ 19 | export function getRootSkeleton() { 20 | if (arguments.length === 0) { 21 | return [{}, null] 22 | } 23 | 24 | const skeleton = { 25 | name: arguments[0], 26 | children: [], 27 | } 28 | 29 | let childrenWrapper = skeleton.children 30 | 31 | const args = Array.prototype.slice.call(arguments, 1) 32 | args.forEach(function(argument) { 33 | const level = { 34 | name: argument, 35 | children: [], 36 | } 37 | childrenWrapper.push(level) 38 | childrenWrapper = level.children 39 | }) 40 | 41 | return [skeleton, childrenWrapper] 42 | } 43 | 44 | /** 45 | * serializes an simple xml representation into a string 46 | * 47 | * @param {object} json 48 | * @return {string} 49 | */ 50 | export function serialize(json) { 51 | json = json || {} 52 | if (typeof json !== 'object' || !Object.prototype.hasOwnProperty.call(json, 'name')) { 53 | return '' 54 | } 55 | 56 | const root = document.implementation.createDocument('', '', null) 57 | xmlify(root, root, json) 58 | 59 | return serializer.serializeToString(root) 60 | } 61 | 62 | function xmlify(xmlDoc, parent, json) { 63 | const [ns, localName] = json.name 64 | const element = xmlDoc.createElementNS(ns, getPrefixedNameForNamespace(ns, localName)) 65 | 66 | json.attributes = json.attributes || [] 67 | json.attributes.forEach((attribute) => { 68 | if (attribute.length === 2) { 69 | const [name, value] = attribute 70 | element.setAttribute(name, value) 71 | } else { 72 | const [namespace, localName, value] = attribute 73 | element.setAttributeNS(namespace, getPrefixedNameForNamespace(namespace, localName), value) 74 | } 75 | }) 76 | 77 | if (json.value) { 78 | element.textContent = json.value 79 | } else if (json.children) { 80 | json.children.forEach((child) => { 81 | xmlify(xmlDoc, element, child) 82 | }) 83 | } 84 | 85 | parent.appendChild(element) 86 | } 87 | 88 | export function resetPrefixMap() { 89 | prefixMap = {} 90 | } 91 | 92 | function getPrefixedNameForNamespace(ns, localName) { 93 | if (!Object.prototype.hasOwnProperty.call(prefixMap, ns)) { 94 | prefixMap[ns] = 'x' + Object.keys(prefixMap).length 95 | } 96 | 97 | return prefixMap[ns] + ':' + localName 98 | } 99 | -------------------------------------------------------------------------------- /test/assets/unit/parser/dprop.xml: -------------------------------------------------------------------------------- 1 | 2 | Privat 3 | 4 | 5 | 6 | 7 | 0 8 | #78e774 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 1 18 | 19 | 20 | 21 | /remote.php/dav/principals/users/admin/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | /remote.php/dav/principals/users/admin/ 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | /remote.php/dav/principals/users/admin/ 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/mocks/davCollection.mock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { vi } from "vitest"; 7 | 8 | const DavCollection = vi.fn() 9 | 10 | DavCollection.prototype.findAll = vi.fn(); 11 | DavCollection.prototype.findAllByFilter = vi.fn(); 12 | DavCollection.prototype.find = vi.fn(); 13 | DavCollection.prototype.createCollection = vi.fn(); 14 | DavCollection.prototype.createObject = vi.fn(); 15 | DavCollection.prototype.update = vi.fn(); 16 | DavCollection.prototype.delete = vi.fn(); 17 | DavCollection.prototype.isReadable = vi.fn(); 18 | DavCollection.prototype.isWriteable = vi.fn(); 19 | DavCollection.prototype.isSameCollectionTypeAs = vi.fn(); 20 | 21 | export { DavCollection }; 22 | -------------------------------------------------------------------------------- /test/mocks/request.mock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { vi } from "vitest"; 7 | 8 | const Request = vi.fn() 9 | 10 | Request.prototype.propFind = vi.fn(); 11 | Request.prototype.put = vi.fn(); 12 | Request.prototype.post = vi.fn(); 13 | Request.prototype.delete = vi.fn(); 14 | Request.prototype.move = vi.fn(); 15 | Request.prototype.copy = vi.fn(); 16 | Request.prototype.report = vi.fn(); 17 | Request.prototype.pathname = vi.fn(); 18 | Request.prototype.propPatch = vi.fn(); 19 | Request.prototype.mkCol = vi.fn(); 20 | 21 | export default Request 22 | -------------------------------------------------------------------------------- /test/unit/clientTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { describe, expect, it, vi } from 'vitest' 7 | 8 | import Client from '../../src/index.js' 9 | 10 | describe('Client', () => { 11 | it('should extract advertised DAV features', () => { 12 | const headers = { 13 | dav: '1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, oc-resource-sharing, calendar-access, calendar-proxy, calendar-auto-schedule, calendar-availability, nc-calendar-trashbin, nc-calendar-webcal-cache, calendarserver-subscribed, oc-calendar-publishing, calendarserver-sharing, addressbook, nc-paginate, nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar', 14 | foobar: 'baz', 15 | } 16 | 17 | const client = new Client({ 18 | rootUrl: '/foobar/', 19 | }) 20 | client._extractAdvertisedDavFeatures(headers) 21 | expect(client.advertisedFeatures).toEqual([ 22 | '1', 23 | '3', 24 | 'extended-mkcol', 25 | 'access-control', 26 | 'calendarserver-principal-property-search', 27 | 'oc-resource-sharing', 28 | 'calendar-access', 29 | 'calendar-proxy', 30 | 'calendar-auto-schedule', 31 | 'calendar-availability', 32 | 'nc-calendar-trashbin', 33 | 'nc-calendar-webcal-cache', 34 | 'calendarserver-subscribed', 35 | 'oc-calendar-publishing', 36 | 'calendarserver-sharing', 37 | 'addressbook', 38 | 'nc-paginate', 39 | 'nextcloud-checksum-update', 40 | 'nc-calendar-search', 41 | 'nc-enable-birthday-calendar', 42 | ]) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/unit/debugTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { afterEach, describe, expect, it, vi } from "vitest"; 11 | 12 | import { debugFactory } from '../../src/debug.js'; 13 | 14 | describe('Debug', () => { 15 | 16 | afterEach(() => { 17 | debugFactory.enabled = false; 18 | }); 19 | 20 | it ('should provide a factory for a debugger', () => { 21 | expect(debugFactory).toEqual(expect.any(Function)); 22 | expect(debugFactory('foo')).toEqual(expect.any(Function)); 23 | }); 24 | 25 | it ('should log console messages including their context if debug is enabled', () => { 26 | vi.spyOn(window.console, 'debug').mockImplementation(() => {}); 27 | 28 | debugFactory.enabled = true; 29 | 30 | const debug = debugFactory('foo'); 31 | 32 | debug(123); 33 | expect(window.console.debug).toHaveBeenCalledWith('foo', 123); 34 | 35 | debug(123, 456); 36 | expect(window.console.debug).toHaveBeenCalledWith('foo', 123, 456); 37 | }); 38 | 39 | it ('should not log console messages if debug is disabled', () => { 40 | vi.spyOn(window.console, 'debug').mockImplementation(() => {}); 41 | 42 | debugFactory.enabled = false; 43 | 44 | const debug = debugFactory('foo'); 45 | debug(123); 46 | 47 | expect(window.console.debug).not.toHaveBeenCalledWith('foo', 123); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/unit/models/addressBookHomeTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { assert, beforeEach, describe, expect, it, vi } from "vitest"; 11 | 12 | import * as XMLUtility from "../../../src/utility/xmlUtility.js"; 13 | import {AddressBookHome} from "../../../src/models/addressBookHome.js"; 14 | import {DavCollection} from "../../../src/models/davCollection.js"; 15 | import {AddressBook} from "../../../src/models/addressBook.js"; 16 | import RequestMock from "../../mocks/request.mock.js"; 17 | 18 | describe('Address book home model', () => { 19 | 20 | beforeEach(() => { 21 | XMLUtility.resetPrefixMap(); 22 | }); 23 | 24 | it('should inherit from DavCollection', () => { 25 | const parent = null; 26 | const request = new RequestMock(); 27 | const url = '/nextcloud/remote.php/dav/addressbooks/users/admin/'; 28 | 29 | const addressBookHome = new AddressBookHome(parent, request, url, {}); 30 | expect(addressBookHome).toEqual(expect.any(DavCollection)); 31 | }); 32 | 33 | it('should find all address-books', () => { 34 | const parent = null; 35 | const request = new RequestMock(); 36 | const url = '/nextcloud/remote.php/dav/addressbooks/users/admin/'; 37 | 38 | request.propFind.mockImplementation(() => { 39 | return Promise.resolve({ 40 | status: 207, 41 | body: getDefaultPropFind(), 42 | headers: {} 43 | }); 44 | }); 45 | 46 | request.pathname.mockImplementation((p) => p); 47 | 48 | const addressBookHome = new AddressBookHome(parent, request, url, {}); 49 | return addressBookHome.findAllAddressBooks().then((res) => { 50 | expect(res.length).toEqual(1); 51 | expect(res[0]).toEqual(expect.any(AddressBook)); 52 | expect(res[0].url).toEqual('/nextcloud/remote.php/dav/addressbooks/users/admin/contacts/'); 53 | 54 | expect(request.propFind).toHaveBeenCalledTimes(1); 55 | expect(request.propFind).toHaveBeenCalledWith('/nextcloud/remote.php/dav/addressbooks/users/admin/', expect.any(Array), 1); 56 | }).catch(() => { 57 | assert.fail('AddressBookGome findAllAddressBooks was not supposed to assert.fail'); 58 | }); 59 | }); 60 | 61 | it('should create a new address-book collection', () => { 62 | const parent = null; 63 | const request = new RequestMock(); 64 | const url = '/nextcloud/remote.php/dav/addressbooks/users/admin/'; 65 | 66 | request.propFind.mockReturnValueOnce(Promise.resolve({ 67 | status: 207, 68 | body: getDefaultPropFind(), 69 | headers: {} 70 | })).mockReturnValueOnce(Promise.resolve({ 71 | status: 207, 72 | body: { 73 | "{DAV:}resourcetype" : [ 74 | "{DAV:}collection", 75 | "{urn:ietf:params:xml:ns:carddav}addressbook" 76 | ], 77 | "{DAV:}displayname" : "Renamed Address book", 78 | "{urn:ietf:params:xml:ns:carddav}addressbook-description": 'This is a fancy description', 79 | }, 80 | headers: {} 81 | })); 82 | 83 | request.pathname.mockImplementation((p) => p); 84 | 85 | request.mkCol.mockImplementation(() => { 86 | return Promise.resolve({ 87 | status: 201, 88 | body: null, 89 | headers: {} 90 | }) 91 | }); 92 | 93 | const addressBookHome = new AddressBookHome(parent, request, url, {}); 94 | return addressBookHome.findAllAddressBooks().then(() => { 95 | return addressBookHome.createAddressBookCollection('contacts').then((res) => { 96 | expect(res).toEqual(expect.any(AddressBook)); 97 | expect(res.url).toEqual('/nextcloud/remote.php/dav/addressbooks/users/admin/contacts-1/'); 98 | 99 | expect(request.propFind).toHaveBeenCalledTimes(2); 100 | expect(request.propFind).toHaveBeenCalledWith('/nextcloud/remote.php/dav/addressbooks/users/admin/', expect.any(Array), 1); 101 | expect(request.propFind).toHaveBeenCalledWith('/nextcloud/remote.php/dav/addressbooks/users/admin/contacts-1/', expect.any(Array), 0); 102 | 103 | expect(request.mkCol).toHaveBeenCalledTimes(1); 104 | expect(request.mkCol).toHaveBeenCalledWith('/nextcloud/remote.php/dav/addressbooks/users/admin/contacts-1', {}, 105 | 'contacts'); 106 | }).catch(() => { 107 | assert.fail('AddressBookGome createAddressBookCollection was not supposed to assert.fail'); 108 | }); 109 | }).catch(() => { 110 | assert.fail('AddressBookGome createAddressBookCollection was not supposed to assert.fail'); 111 | }); 112 | }); 113 | 114 | }); 115 | 116 | function getDefaultPropFind() { 117 | return { 118 | "/nextcloud/remote.php/dav/addressbooks/users/admin/" : { 119 | "{DAV:}resourcetype" : [ 120 | "{DAV:}collection" 121 | ], 122 | "{DAV:}owner" : "/nextcloud/remote.php/dav/principals/users/admin/", 123 | "{DAV:}current-user-privilege-set" : [ 124 | "{DAV:}all", 125 | "{DAV:}read", 126 | "{DAV:}write", 127 | "{DAV:}write-properties", 128 | "{DAV:}write-content", 129 | "{DAV:}unlock", 130 | "{DAV:}bind", 131 | "{DAV:}unbind", 132 | "{DAV:}write-acl", 133 | "{DAV:}read-acl", 134 | "{DAV:}read-current-user-privilege-set" 135 | ] 136 | }, 137 | "/nextcloud/remote.php/dav/addressbooks/users/admin/contacts/" : { 138 | "{DAV:}resourcetype" : [ 139 | "{DAV:}collection", 140 | "{urn:ietf:params:xml:ns:carddav}addressbook" 141 | ], 142 | "{DAV:}displayname" : "Renamed Address book", 143 | "{DAV:}owner" : "/nextcloud/remote.php/dav/principals/users/admin/", 144 | "{DAV:}sync-token" : "http://sabre.io/ns/sync/7", 145 | "{DAV:}current-user-privilege-set" : [ 146 | "{DAV:}write", 147 | "{DAV:}write-properties", 148 | "{DAV:}write-content", 149 | "{DAV:}unlock", 150 | "{DAV:}bind", 151 | "{DAV:}unbind", 152 | "{DAV:}write-acl", 153 | "{DAV:}read", 154 | "{DAV:}read-acl", 155 | "{DAV:}read-current-user-privilege-set" 156 | ], 157 | "{http://owncloud.org/ns}invite" : [ 158 | 159 | ], 160 | "{urn:ietf:params:xml:ns:carddav}supported-address-data" : [ 161 | { 162 | "content-type" : "text/vcard", 163 | "version" : "3.0" 164 | }, 165 | { 166 | "content-type" : "text/vcard", 167 | "version" : "4.0" 168 | }, 169 | { 170 | "content-type" : "application/vcard+json", 171 | "version" : "4.0" 172 | } 173 | ], 174 | "{urn:ietf:params:xml:ns:carddav}max-resource-size" : 10000000, 175 | "{http://calendarserver.org/ns/}getctag" : "7" 176 | }, 177 | '/nextcloud/remote.php/dav/addressbooks/users/admin/d': { 178 | '{DAV:}displayname': 'Foo Bar Bla Blub col0', 179 | '{DAV:}owner': 'https://foo/bar/', 180 | '{DAV:}resourcetype': ['{DAV:}collection'], 181 | '{DAV:}sync-token': 'https://foo/bar/token/3', 182 | }, 183 | }; 184 | } 185 | -------------------------------------------------------------------------------- /test/unit/models/addressBookTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { assert, beforeEach, describe, expect, it, vi } from "vitest"; 11 | 12 | import { DavCollection } from "../../../src/models/davCollection.js"; 13 | import {AddressBook} from "../../../src/models/addressBook.js"; 14 | import * as XMLUtility from "../../../src/utility/xmlUtility.js"; 15 | import {VCard} from "../../../src/models/vcard.js"; 16 | import * as NS from "../../../src/utility/namespaceUtility.js"; 17 | import RequestMock from "../../mocks/request.mock.js"; 18 | import { DavCollection as DavCollectionMock } from "../../mocks/davCollection.mock.js"; 19 | 20 | describe('Address book model', () => { 21 | 22 | beforeEach(() => { 23 | XMLUtility.resetPrefixMap(); 24 | }); 25 | 26 | it('should inherit from DavCollection / shareable ', () => { 27 | const parent = { 28 | 'findAll': vi.fn(), 29 | 'findAllByFilter': vi.fn(), 30 | 'find': vi.fn(), 31 | 'createCollection': vi.fn(), 32 | 'createObject': vi.fn(), 33 | 'update': vi.fn(), 34 | 'delete': vi.fn(), 35 | 'isReadable': vi.fn(), 36 | 'isWriteable': vi.fn() 37 | }; 38 | const request = new RequestMock(); 39 | const url = '/foo/bar/folder'; 40 | const props = returnDefaultProps(); 41 | 42 | const addressbook = new AddressBook(parent, request, url, props); 43 | expect(addressbook).toEqual(expect.any(DavCollection)); 44 | expect(addressbook.share).toEqual(expect.any(Function)); 45 | expect(addressbook.unshare).toEqual(expect.any(Function)); 46 | }); 47 | 48 | it('should inherit expose the property description', () => { 49 | const parent = new DavCollectionMock(); 50 | const request = new RequestMock(); 51 | const url = '/foo/bar/folder'; 52 | const props = returnDefaultProps(); 53 | 54 | const addressbook = new AddressBook(parent, request, url, props); 55 | expect(addressbook.description).toEqual('This is a fancy description'); 56 | }); 57 | 58 | it('should inherit expose the property enabled', () => { 59 | const parent = new DavCollectionMock(); 60 | const request = new RequestMock(); 61 | const url = '/foo/bar/folder'; 62 | const props = returnDefaultProps(); 63 | 64 | const addressbook = new AddressBook(parent, request, url, props); 65 | expect(addressbook.enabled).toEqual(true); 66 | }); 67 | 68 | it('should inherit expose the property read-only', () => { 69 | const parent = new DavCollectionMock(); 70 | const request = new RequestMock(); 71 | const url = '/foo/bar/folder'; 72 | const props = returnDefaultProps(); 73 | 74 | const addressbook = new AddressBook(parent, request, url, props); 75 | expect(addressbook.readOnly).toEqual(false); 76 | }); 77 | 78 | it('should find all VCards', () => { 79 | const parent = new DavCollectionMock(); 80 | const request = new RequestMock(); 81 | const url = '/foo/bar/folder'; 82 | const props = returnDefaultProps(); 83 | 84 | request.propFind.mockImplementation(() => { 85 | return Promise.resolve({ 86 | status: 207, 87 | body: { 88 | '/foo/bar/folder/a': getVCardProps(), 89 | '/foo/bar/folder/b': getVCardProps(), 90 | '/foo/bar/folder/c': { 91 | '{DAV:}owner': 'https://foo/bar/', 92 | '{DAV:}resourcetype': [], 93 | '{DAV:}sync-token': 'https://foo/bar/token/3', 94 | '{DAV:}getcontenttype': 'text/foo1; charset=utf8' 95 | }, 96 | }, 97 | headers: {} 98 | }); 99 | }); 100 | 101 | request.pathname.mockImplementation((p) => p); 102 | 103 | const addressbook = new AddressBook(parent, request, url, props); 104 | return addressbook.findAllVCards().then((res) => { 105 | expect(res.length).toEqual(2); 106 | expect(res[0]).toEqual(expect.any(VCard)); 107 | expect(res[0].url).toEqual('/foo/bar/folder/a'); 108 | expect(res[1]).toEqual(expect.any(VCard)); 109 | expect(res[1].url).toEqual('/foo/bar/folder/b'); 110 | 111 | expect(request.propFind).toHaveBeenCalledTimes(1); 112 | expect(request.propFind).toHaveBeenCalledWith('/foo/bar/folder/', [ 113 | ['DAV:', 'getcontenttype'], ['DAV:', 'getetag'], ['DAV:', 'resourcetype'], 114 | ['DAV:', 'displayname'], ['DAV:', 'owner'], ['DAV:', 'resourcetype'], 115 | ['DAV:', 'sync-token'], ['DAV:', 'current-user-privilege-set'], 116 | ['DAV:', 'getcontenttype'], ['DAV:', 'getetag'], ['DAV:', 'resourcetype'], 117 | ['urn:ietf:params:xml:ns:carddav', 'address-data']], 1); 118 | }).catch(() => { 119 | assert.fail('Addressbook findAllVCards was not supposed to assert.fail'); 120 | }); 121 | }); 122 | 123 | it('should find all VCards and request only a set of VCard properties', () => { 124 | const parent = new DavCollectionMock(); 125 | const request = new RequestMock(); 126 | const url = '/foo/bar/folder'; 127 | const props = returnDefaultProps(); 128 | 129 | request.report.mockImplementation(() => { 130 | return Promise.resolve({ 131 | status: 207, 132 | body: { 133 | '/foo/bar/folder/b': getVCardProps() 134 | }, 135 | headers: {} 136 | }); 137 | }); 138 | 139 | request.pathname.mockImplementation((p) => p); 140 | 141 | const addressbook = new AddressBook(parent, request, url, props); 142 | return addressbook.findAllAndFilterBySimpleProperties(['EMAIL', 'UID', 'CATEGORIES']).then((res) => { 143 | expect(res.length).toEqual(1); 144 | expect(res[0]).toEqual(expect.any(VCard)); 145 | expect(res[0].url).toEqual('/foo/bar/folder/b'); 146 | 147 | expect(request.report).toHaveBeenCalledTimes(1); 148 | expect(request.report).toHaveBeenCalledWith('/foo/bar/folder/', { Depth: '1' }, 149 | ''); 150 | }).catch(() => { 151 | assert.fail('AddressBook findAllAndFilterBySimpleProperties was not supposed to assert.fail'); 152 | }); 153 | }); 154 | 155 | it('should create a new VCard', () => { 156 | const parent = new DavCollectionMock(); 157 | const request = new RequestMock(); 158 | const url = '/foo/bar/folder'; 159 | const props = returnDefaultProps(); 160 | 161 | request.put.mockImplementation(() => { 162 | return Promise.resolve({ 163 | status: 204, 164 | body: null, 165 | headers: {}, 166 | }) 167 | }); 168 | request.propFind.mockImplementation(() => { 169 | return Promise.resolve({ 170 | status: 207, 171 | body: getVCardProps(), 172 | headers: {} 173 | }); 174 | }); 175 | request.pathname.mockImplementation((p) => p); 176 | 177 | const addressbook = new AddressBook(parent, request, url, props); 178 | return addressbook.createVCard('DATA123').then((res) => { 179 | expect(res).toEqual(expect.any(VCard)); 180 | expect(res.url).toEqual(expect.any(String)); 181 | expect(res.url.startsWith('/foo/bar/folder/')).toEqual(true); 182 | expect(res.url.endsWith('.vcf')).toEqual(true); 183 | expect(res.etag).toEqual('"095329048d1a5a7ce26ec24bb7af0908"'); 184 | 185 | expect(request.put).toHaveBeenCalledTimes(1); 186 | expect(request.put).toHaveBeenCalledWith(expect.any(String), { 'Content-Type': 'text/vcard; charset=utf-8' }, 'DATA123'); 187 | expect(request.propFind).toHaveBeenCalledTimes(1); 188 | expect(request.propFind).toHaveBeenCalledWith(expect.any(String), [ 189 | ['DAV:', 'getcontenttype'], ['DAV:', 'getetag'], ['DAV:', 'resourcetype'], 190 | ['DAV:', 'displayname'], ['DAV:', 'owner'], ['DAV:', 'resourcetype'], 191 | ['DAV:', 'sync-token'], ['DAV:', 'current-user-privilege-set'], 192 | ['DAV:', 'getcontenttype'], ['DAV:', 'getetag'], ['DAV:', 'resourcetype'], 193 | ['urn:ietf:params:xml:ns:carddav', 'address-data']], 0); 194 | }).catch(() => { 195 | assert.fail('DavCollection update was not supposed to assert.fail'); 196 | }); 197 | }); 198 | 199 | it('should send an addressbook-query', () => { 200 | const parent = new DavCollectionMock(); 201 | const request = new RequestMock(); 202 | const url = '/foo/bar/folder'; 203 | const props = returnDefaultProps(); 204 | 205 | request.report.mockImplementation(() => { 206 | return Promise.resolve({ 207 | status: 207, 208 | body: { 209 | '/foo/bar/folder/b': getVCardProps() 210 | }, 211 | headers: {} 212 | }); 213 | }); 214 | 215 | request.pathname.mockImplementation((p) => p); 216 | 217 | // https://tools.ietf.org/html/rfc6352#section-8.6.4 218 | const addressbook = new AddressBook(parent, request, url, props); 219 | return addressbook.addressbookQuery([{ 220 | name: [NS.IETF_CARDDAV, 'prop-filter'], 221 | attributes: [ 222 | ['name', 'FN'] 223 | ], 224 | children: [{ 225 | name: [NS.IETF_CARDDAV, 'text-match'], 226 | attributes: [ 227 | ['collation', 'i;unicode-casemap'], 228 | ['match-type', 'contains'], 229 | ], 230 | value: 'daboo' 231 | }] 232 | }, { 233 | name: [NS.IETF_CARDDAV, 'prop-filter'], 234 | attributes: [ 235 | ['name', 'EMAIL'] 236 | ], 237 | children: [{ 238 | name: [NS.IETF_CARDDAV, 'text-match'], 239 | attributes: [ 240 | ['collation', 'i;unicode-casemap'], 241 | ['match-type', 'contains'], 242 | ], 243 | value: 'daboo' 244 | }] 245 | }]).then((res) => { 246 | expect(res.length).toEqual(1); 247 | expect(res[0]).toEqual(expect.any(VCard)); 248 | expect(res[0].url).toEqual('/foo/bar/folder/b'); 249 | 250 | expect(request.report).toHaveBeenCalledTimes(1); 251 | expect(request.report).toHaveBeenCalledWith('/foo/bar/folder/', { Depth: '1' }, 252 | 'daboodaboo'); 253 | }).catch(() => { 254 | assert.fail('AddressBook addressbook-query was not supposed to assert.fail'); 255 | }); 256 | }); 257 | 258 | it('should send an addressbook-multiget', () => { 259 | const parent = new DavCollectionMock(); 260 | const request = new RequestMock(); 261 | const url = '/foo/bar/folder'; 262 | const props = returnDefaultProps(); 263 | 264 | request.report.mockImplementation(() => { 265 | return Promise.resolve({ 266 | status: 207, 267 | body: { 268 | '/foo/bar/folder/a': getVCardProps(), 269 | '/foo/bar/folder/b': getVCardProps() 270 | }, 271 | headers: {} 272 | }); 273 | }); 274 | 275 | request.pathname.mockImplementation((p) => p); 276 | 277 | const addressbook = new AddressBook(parent, request, url, props); 278 | return addressbook.addressbookMultiget(['/foo/bar/folder/a', '/foo/bar/folder/b']).then((res) => { 279 | expect(res.length).toEqual(2); 280 | expect(res[0]).toEqual(expect.any(VCard)); 281 | expect(res[0].url).toEqual('/foo/bar/folder/a'); 282 | expect(res[1]).toEqual(expect.any(VCard)); 283 | expect(res[1].url).toEqual('/foo/bar/folder/b'); 284 | 285 | expect(request.report).toHaveBeenCalledTimes(1); 286 | expect(request.report).toHaveBeenCalledWith('/foo/bar/folder/', { Depth: '1' }, 287 | '/foo/bar/folder/a/foo/bar/folder/b'); 288 | }).catch(() => { 289 | assert.fail('AddressBook addressbook-multiget was not supposed to assert.fail'); 290 | }); 291 | }); 292 | 293 | it('should send an addressbook-multiget and request export of data', () => { 294 | const parent = new DavCollectionMock(); 295 | const request = new RequestMock(); 296 | const url = '/foo/bar/folder'; 297 | const props = returnDefaultProps(); 298 | 299 | request.report.mockImplementation(() => { 300 | return Promise.resolve({ 301 | status: 200, 302 | body: 'RAW DATA', 303 | headers: {} 304 | }); 305 | }); 306 | 307 | request.pathname.mockImplementation((p) => p); 308 | 309 | const addressbook = new AddressBook(parent, request, url, props); 310 | return addressbook.addressbookMultigetExport(['/foo/bar/folder/a', '/foo/bar/folder/b']).then((res) => { 311 | expect(res.status).toEqual(200); 312 | expect(res.body).toEqual('RAW DATA'); 313 | 314 | expect(request.report).toHaveBeenCalledTimes(1); 315 | expect(request.report).toHaveBeenCalledWith('/foo/bar/folder/?export', { Depth: '1' }, 316 | '/foo/bar/folder/a/foo/bar/folder/b'); 317 | }).catch(() => { 318 | assert.fail('AddressBook addressbook-multiget was not supposed to assert.fail'); 319 | }); 320 | }); 321 | }); 322 | 323 | function returnDefaultProps() { 324 | return { 325 | "{DAV:}resourcetype" : [ 326 | "{DAV:}collection", 327 | "{urn:ietf:params:xml:ns:carddav}addressbook" 328 | ], 329 | "{DAV:}displayname" : "Renamed Address book", 330 | "{urn:ietf:params:xml:ns:carddav}addressbook-description": 'This is a fancy description', 331 | "{http://owncloud.org/ns}enabled": true, 332 | "{http://owncloud.org/ns}read-only": false, 333 | "{DAV:}owner" : "/nextcloud/remote.php/dav/principals/users/admin/", 334 | "{DAV:}sync-token" : "http://sabre.io/ns/sync/7", 335 | "{DAV:}current-user-privilege-set" : [ 336 | "{DAV:}write", 337 | "{DAV:}write-properties", 338 | "{DAV:}write-content", 339 | "{DAV:}unlock", 340 | "{DAV:}bind", 341 | "{DAV:}unbind", 342 | "{DAV:}write-acl", 343 | "{DAV:}read", 344 | "{DAV:}read-acl", 345 | "{DAV:}read-current-user-privilege-set" 346 | ], 347 | "{http://owncloud.org/ns}invite" : [ 348 | 349 | ], 350 | "{urn:ietf:params:xml:ns:carddav}supported-address-data" : [ 351 | { 352 | "content-type" : "text/vcard", 353 | "version" : "3.0" 354 | }, 355 | { 356 | "content-type" : "text/vcard", 357 | "version" : "4.0" 358 | }, 359 | { 360 | "content-type" : "application/vcard+json", 361 | "version" : "4.0" 362 | } 363 | ], 364 | "{urn:ietf:params:xml:ns:carddav}max-resource-size" : 10000000, 365 | "{http://calendarserver.org/ns/}getctag" : "7" 366 | }; 367 | } 368 | 369 | function getVCardProps() { 370 | return { 371 | "{DAV:}getcontenttype" : "text/vcard; charset=utf-8", 372 | "{DAV:}getetag" : "\"095329048d1a5a7ce26ec24bb7af0908\"", 373 | "{DAV:}resourcetype" : [ 374 | 375 | ], 376 | "{DAV:}owner" : "/nextcloud/remote.php/dav/principals/users/admin/", 377 | "{DAV:}current-user-privilege-set" : [ 378 | "{DAV:}write", 379 | "{DAV:}write-properties", 380 | "{DAV:}write-content", 381 | "{DAV:}unlock", 382 | "{DAV:}write-acl", 383 | "{DAV:}read", 384 | "{DAV:}read-acl", 385 | "{DAV:}read-current-user-privilege-set" 386 | ], 387 | "{urn:ietf:params:xml:ns:carddav}address-data" : "BEGIN:VCARD\nVERSION:3.0\nPRODID:-//Apple Inc.//iOS 10.2//EN\nN:Foo;Bar;;;\nFN:BAR FOOOOO\nEMAIL;TYPE=INTERNET,WORK,pref:foo@example.com\nITEM1.TEL;TYPE=pref:+1800FOOBAR\nITEM1.X-ABLABEL:WORK,VOICE\nREV:2017-01-02T14:44:35Z\nUID:ad52f2c5-6444-4f2a-8e07-8f3a822855de\nEND:VCARD" 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /test/unit/models/davCollectionPublishableTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { beforeEach, describe, expect, it, vi } from "vitest"; 11 | 12 | import { davCollectionPublishable } from '../../../src/models/davCollectionPublishable.js'; 13 | import * as XMLUtility from "../../../src/utility/xmlUtility.js"; 14 | 15 | describe('Publishable dav collection model', () => { 16 | 17 | beforeEach(() => { 18 | XMLUtility.resetPrefixMap(); 19 | }); 20 | 21 | it('should extend the base class and expose a publishURL property', () => { 22 | function Foo() {} 23 | Foo.prototype._request = { 24 | 'post': vi.fn() 25 | }; 26 | Foo.prototype._exposeProperty = vi.fn(); 27 | 28 | const share = new (davCollectionPublishable(Foo))(); 29 | expect(Foo.prototype._exposeProperty).toHaveBeenCalledTimes(1); 30 | expect(Foo.prototype._exposeProperty).toHaveBeenCalledWith('publishURL', 'http://calendarserver.org/ns/', 'publish-url'); 31 | 32 | expect(share).toEqual(expect.any(Foo)); 33 | }); 34 | 35 | it('should provide a publish method', () => { 36 | function Foo() {} 37 | Foo.prototype._request = { 38 | 'post': vi.fn() 39 | }; 40 | Foo.prototype._exposeProperty = vi.fn(); 41 | Foo.prototype._updatePropsFromServer = vi.fn(); 42 | Foo.prototype._url = '/foo'; 43 | Foo.prototype.shares = []; 44 | 45 | Foo.prototype._request.post.mockImplementation(() => Promise.resolve({})); 46 | Foo.prototype._updatePropsFromServer.mockImplementation(() => Promise.resolve({})); 47 | 48 | const share = new (davCollectionPublishable(Foo))(); 49 | return share.publish('principal:foo/a').then(() => { 50 | expect(Foo.prototype._request.post).toHaveBeenCalledTimes(1); 51 | expect(Foo.prototype._request.post).toHaveBeenCalledWith('/foo', { 'Content-Type': 'application/xml; charset=utf-8' }, 52 | ''); 53 | 54 | expect(Foo.prototype._updatePropsFromServer).toHaveBeenCalledTimes(1); 55 | }); 56 | }); 57 | 58 | it('should provide a unpublish method', () => { 59 | function Foo() {} 60 | Foo.prototype._request = { 61 | 'post': vi.fn() 62 | }; 63 | Foo.prototype._exposeProperty = vi.fn(); 64 | Foo.prototype._url = '/foo'; 65 | Foo.prototype._props = []; 66 | Foo.prototype._props['{http://calendarserver.org/ns/}publish-url'] = 'foo-bar'; 67 | 68 | 69 | Foo.prototype._request.post.mockImplementation(() => Promise.resolve({})); 70 | 71 | const share = new (davCollectionPublishable(Foo))(); 72 | expect(share._props['{http://calendarserver.org/ns/}publish-url']).toEqual('foo-bar'); 73 | return share.unpublish('principal:foo/a').then(() => { 74 | expect(Foo.prototype._request.post).toHaveBeenCalledTimes(1); 75 | expect(Foo.prototype._request.post).toHaveBeenCalledWith('/foo', { 'Content-Type': 'application/xml; charset=utf-8' }, 76 | ''); 77 | 78 | expect(share._props['{http://calendarserver.org/ns/}publish-url']).toEqual(undefined); 79 | }); 80 | }); 81 | 82 | it('should provide a getPropFindList method', () => { 83 | function Foo() {} 84 | Foo.prototype._request = { 85 | 'post': vi.fn() 86 | }; 87 | Foo.prototype._exposeProperty = vi.fn(); 88 | Foo.prototype._url = '/foo'; 89 | Foo.getPropFindList = () => { 90 | return [['Foo', 'BAR']]; 91 | }; 92 | 93 | const shareClass = davCollectionPublishable(Foo); 94 | expect(shareClass.getPropFindList()).toEqual([ 95 | ['Foo', 'BAR'], ['http://calendarserver.org/ns/', 'publish-url'] 96 | ]); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/unit/models/davCollectionShareableTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { beforeEach, describe, expect, it, vi } from "vitest"; 11 | 12 | import { davCollectionShareable } from '../../../src/models/davCollectionShareable.js'; 13 | import * as XMLUtility from "../../../src/utility/xmlUtility.js"; 14 | 15 | describe('Shareable dav collection model', () => { 16 | 17 | beforeEach(() => { 18 | XMLUtility.resetPrefixMap(); 19 | }); 20 | 21 | it('should extend the base class and expose two properties', () => { 22 | function Foo() {} 23 | Foo.prototype._request = { 24 | 'post': vi.fn() 25 | }; 26 | Foo.prototype._exposeProperty = vi.fn(); 27 | 28 | const share = new (davCollectionShareable(Foo))(); 29 | expect(Foo.prototype._exposeProperty).toHaveBeenCalledTimes(2); 30 | expect(Foo.prototype._exposeProperty).toHaveBeenCalledWith('shares', 'http://owncloud.org/ns', 'invite'); 31 | expect(Foo.prototype._exposeProperty).toHaveBeenCalledWith('allowedSharingModes', 'http://calendarserver.org/ns/', 'allowed-sharing-modes'); 32 | 33 | expect(share).toEqual(expect.any(Foo)); 34 | }); 35 | 36 | it('should provide a share method - new read only share', () => { 37 | function Foo() {} 38 | Foo.prototype._request = { 39 | 'post': vi.fn() 40 | }; 41 | Foo.prototype._exposeProperty = vi.fn(); 42 | Foo.prototype._url = '/foo'; 43 | Foo.prototype.shares = []; 44 | 45 | Foo.prototype._request.post.mockImplementation(() => Promise.resolve({})); 46 | 47 | const share = new (davCollectionShareable(Foo))(); 48 | return share.share('principal:foo/a').then(() => { 49 | expect(share.shares).toEqual([{ 50 | href: 'principal:foo/a', 51 | access: ['{http://owncloud.org/ns}read'], 52 | 'common-name': null, 53 | 'invite-accepted': true 54 | }]); 55 | 56 | expect(Foo.prototype._request.post).toHaveBeenCalledTimes(1); 57 | expect(Foo.prototype._request.post).toHaveBeenCalledWith('/foo', { 'Content-Type': 'application/xml; charset=utf-8' }, 58 | 'principal:foo/a'); 59 | }); 60 | }); 61 | 62 | it('should provide a share method - new read write share', () => { 63 | function Foo() {} 64 | Foo.prototype._request = { 65 | 'post': vi.fn() 66 | }; 67 | Foo.prototype._exposeProperty = vi.fn(); 68 | Foo.prototype._url = '/foo'; 69 | Foo.prototype.shares = []; 70 | 71 | Foo.prototype._request.post.mockImplementation(() => Promise.resolve({})); 72 | 73 | const share = new (davCollectionShareable(Foo))(); 74 | return share.share('principal:foo/a', true).then(() => { 75 | expect(share.shares).toEqual([{ 76 | href: 'principal:foo/a', 77 | access: ['{http://owncloud.org/ns}read-write'], 78 | 'common-name': null, 79 | 'invite-accepted': true 80 | }]); 81 | 82 | expect(Foo.prototype._request.post).toHaveBeenCalledTimes(1); 83 | expect(Foo.prototype._request.post).toHaveBeenCalledWith('/foo', { 'Content-Type': 'application/xml; charset=utf-8' }, 84 | 'principal:foo/a'); 85 | }); 86 | }); 87 | 88 | it('should provide a share method - updated read only -> read-write', () => { 89 | function Foo() {} 90 | Foo.prototype._request = { 91 | 'post': vi.fn() 92 | }; 93 | Foo.prototype._exposeProperty = vi.fn(); 94 | Foo.prototype._url = '/foo'; 95 | Foo.prototype.shares = [{ 96 | href: 'principal:foo/a', 97 | access: ['{http://owncloud.org/ns}read'], 98 | 'common-name': 'Foo Bar', 99 | 'invite-accepted': true 100 | }]; 101 | 102 | Foo.prototype._request.post.mockImplementation(() => Promise.resolve({})); 103 | 104 | const share = new (davCollectionShareable(Foo))(); 105 | return share.share('principal:foo/a', true).then(() => { 106 | expect(share.shares).toEqual([{ 107 | href: 'principal:foo/a', 108 | access: ['{http://owncloud.org/ns}read-write'], 109 | 'common-name': 'Foo Bar', 110 | 'invite-accepted': true 111 | }]); 112 | 113 | expect(Foo.prototype._request.post).toHaveBeenCalledTimes(1); 114 | expect(Foo.prototype._request.post).toHaveBeenCalledWith('/foo', { 'Content-Type': 'application/xml; charset=utf-8' }, 115 | 'principal:foo/a'); 116 | }); 117 | }); 118 | 119 | it('should provide a share method - updated read write -> read only', () => { 120 | function Foo() {} 121 | Foo.prototype._request = { 122 | 'post': vi.fn() 123 | }; 124 | Foo.prototype._exposeProperty = vi.fn(); 125 | Foo.prototype._url = '/foo'; 126 | Foo.prototype.shares = [{ 127 | href: 'principal:foo/a', 128 | access: ['{http://owncloud.org/ns}read-write'], 129 | 'common-name': 'Foo Bar', 130 | 'invite-accepted': true 131 | }]; 132 | 133 | Foo.prototype._request.post.mockImplementation(() => Promise.resolve({})); 134 | 135 | const share = new (davCollectionShareable(Foo))(); 136 | return share.share('principal:foo/a').then(() => { 137 | expect(share.shares).toEqual([{ 138 | href: 'principal:foo/a', 139 | access: ['{http://owncloud.org/ns}read'], 140 | 'common-name': 'Foo Bar', 141 | 'invite-accepted': true 142 | }]); 143 | 144 | expect(Foo.prototype._request.post).toHaveBeenCalledTimes(1); 145 | expect(Foo.prototype._request.post).toHaveBeenCalledWith('/foo', { 'Content-Type': 'application/xml; charset=utf-8' }, 146 | 'principal:foo/a'); 147 | }); 148 | }); 149 | 150 | 151 | it('should provide a unshare method', () => { 152 | function Foo() {} 153 | Foo.prototype._request = { 154 | 'post': vi.fn() 155 | }; 156 | Foo.prototype._exposeProperty = vi.fn(); 157 | Foo.prototype._url = '/foo'; 158 | Foo.prototype.shares = [{ 159 | href: 'principal:foo/a', 160 | access: ['{http://owncloud.org/ns}read-write'], 161 | 'common-name': 'Foo Bar', 162 | 'invite-accepted': true 163 | }]; 164 | 165 | Foo.prototype._request.post.mockImplementation(() => Promise.resolve({})); 166 | 167 | const share = new (davCollectionShareable(Foo))(); 168 | return share.unshare('principal:foo/a').then(() => { 169 | expect(share.shares).toEqual([]); 170 | 171 | expect(Foo.prototype._request.post).toHaveBeenCalledTimes(1); 172 | expect(Foo.prototype._request.post).toHaveBeenCalledWith('/foo', { 'Content-Type': 'application/xml; charset=utf-8' }, 173 | 'principal:foo/a'); 174 | }); 175 | }); 176 | 177 | it('should provide a isShareable method - true', () => { 178 | function Foo() {} 179 | Foo.prototype._request = { 180 | 'post': vi.fn() 181 | }; 182 | Foo.prototype._exposeProperty = vi.fn(); 183 | Foo.prototype._url = '/foo'; 184 | Foo.prototype.allowedSharingModes = ['{http://calendarserver.org/ns/}can-be-shared']; 185 | 186 | const share = new (davCollectionShareable(Foo))(); 187 | expect(share.isShareable()).toEqual(true); 188 | }); 189 | 190 | it('should provide a isShareable method - false', () => { 191 | function Foo() {} 192 | Foo.prototype._request = { 193 | 'post': vi.fn() 194 | }; 195 | Foo.prototype._exposeProperty = vi.fn(); 196 | Foo.prototype._url = '/foo'; 197 | Foo.prototype.allowedSharingModes = ['{http://calendarserver.org/ns/}can-be-published']; 198 | 199 | const share = new (davCollectionShareable(Foo))(); 200 | expect(share.isShareable()).toEqual(false); 201 | }); 202 | 203 | it('should provide a isPublishable method - false', () => { 204 | function Foo() {} 205 | Foo.prototype._request = { 206 | 'post': vi.fn() 207 | }; 208 | Foo.prototype._exposeProperty = vi.fn(); 209 | Foo.prototype._url = '/foo'; 210 | Foo.prototype.allowedSharingModes = ['{http://calendarserver.org/ns/}can-be-shared']; 211 | 212 | const share = new (davCollectionShareable(Foo))(); 213 | expect(share.isPublishable()).toEqual(false); 214 | }); 215 | 216 | it('should provide a isPublishable method - true', () => { 217 | function Foo() {} 218 | Foo.prototype._request = { 219 | 'post': vi.fn() 220 | }; 221 | Foo.prototype._exposeProperty = vi.fn(); 222 | Foo.prototype._url = '/foo'; 223 | Foo.prototype.allowedSharingModes = ['{http://calendarserver.org/ns/}can-be-published']; 224 | 225 | const share = new (davCollectionShareable(Foo))(); 226 | expect(share.isPublishable()).toEqual(true); 227 | }); 228 | 229 | it('should provide a getPropFindList method', () => { 230 | function Foo() {} 231 | Foo.prototype._request = { 232 | 'post': vi.fn() 233 | }; 234 | Foo.prototype._exposeProperty = vi.fn(); 235 | Foo.prototype._url = '/foo'; 236 | Foo.getPropFindList = () => { 237 | return [['Foo', 'BAR']]; 238 | }; 239 | 240 | const shareClass = davCollectionShareable(Foo); 241 | expect(shareClass.getPropFindList()).toEqual([ 242 | ['Foo', 'BAR'], ['http://owncloud.org/ns', 'invite'], 243 | ['http://calendarserver.org/ns/', 'allowed-sharing-modes'] 244 | ]); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /test/unit/models/davEventListenerTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it, vi } from "vitest"; 11 | 12 | import DAVEventListener from "../../../src/models/davEventListener.js"; 13 | 14 | describe('Dav event listener base model', () => { 15 | 16 | it('should allow to register event listeners', () => { 17 | const davEventListener = new DAVEventListener(); 18 | 19 | const handler1 = vi.fn(); 20 | const handler2 = vi.fn(); 21 | 22 | davEventListener.addEventListener('foo', handler1); 23 | davEventListener.addEventListener('bar', handler1); 24 | davEventListener.addEventListener('bar', handler2); 25 | 26 | davEventListener.dispatchEvent('bar', 'FooBar'); 27 | 28 | expect(handler1).toHaveBeenCalledTimes(1); 29 | expect(handler1).toHaveBeenCalledWith('FooBar'); 30 | expect(handler2).toHaveBeenCalledTimes(1); 31 | expect(handler2).toHaveBeenCalledWith('FooBar'); 32 | }); 33 | 34 | it('should allow to remove event listeners', () => { 35 | const davEventListener = new DAVEventListener(); 36 | 37 | const handler1 = vi.fn(); 38 | const handler2 = vi.fn(); 39 | 40 | davEventListener.addEventListener('foo', handler1); 41 | davEventListener.addEventListener('bar', handler1); 42 | davEventListener.addEventListener('bar', handler2); 43 | 44 | davEventListener.removeEventListener('bar', handler1); 45 | 46 | davEventListener.dispatchEvent('bar', 'FooBar'); 47 | 48 | expect(handler1).toHaveBeenCalledTimes(0); 49 | expect(handler2).toHaveBeenCalledTimes(1); 50 | expect(handler2).toHaveBeenCalledWith('FooBar'); 51 | }); 52 | 53 | it('should allow to register event listeners for just one call', () => { 54 | const davEventListener = new DAVEventListener(); 55 | 56 | const handler1 = vi.fn(); 57 | const handler2 = vi.fn(); 58 | 59 | davEventListener.addEventListener('foo', handler1); 60 | davEventListener.addEventListener('bar', handler1, {once: true}); 61 | davEventListener.addEventListener('bar', handler2); 62 | 63 | davEventListener.dispatchEvent('bar', 'Bli Bla blub'); 64 | davEventListener.dispatchEvent('bar', 'Foo Bar biz'); 65 | 66 | expect(handler1).toHaveBeenCalledTimes(1); 67 | expect(handler1).toHaveBeenCalledWith('Bli Bla blub'); 68 | expect(handler2).toHaveBeenCalledTimes(2); 69 | expect(handler2).toHaveBeenCalledWith('Bli Bla blub'); 70 | expect(handler2).toHaveBeenCalledWith('Foo Bar biz'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/unit/models/davEventTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it } from "vitest"; 11 | 12 | import DAVEvent from "../../../src/models/davEvent.js"; 13 | 14 | describe('Dav event model', () => { 15 | it('should provide a constructor with type', () => { 16 | const event1 = new DAVEvent('UPDATED_ON_SERVER'); 17 | 18 | expect(event1).toEqual(expect.any(DAVEvent)); 19 | expect(event1.type).toEqual('UPDATED_ON_SERVER'); 20 | }); 21 | 22 | it('should provide a constructor with type and more options', () => { 23 | const event1 = new DAVEvent('UPDATED_ON_SERVER', { 24 | 'foo': 'bar', 25 | 'bar': 'baz', 26 | }); 27 | 28 | expect(event1).toEqual(expect.any(DAVEvent)); 29 | expect(event1.type).toEqual('UPDATED_ON_SERVER'); 30 | expect(event1.foo).toEqual('bar'); 31 | expect(event1.bar).toEqual('baz'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/models/scheduleInboxTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it } from "vitest"; 11 | 12 | import ScheduleInbox from "../../../src/models/scheduleInbox.js"; 13 | import { Calendar } from "../../../src/models/calendar.js"; 14 | import RequestMock from "../../mocks/request.mock.js"; 15 | import { DavCollection as DavCollectionMock } from "../../mocks/davCollection.mock.js"; 16 | 17 | describe('Schedule inbox model', () => { 18 | 19 | it('should inherit from Calendar', () => { 20 | const parent = new DavCollectionMock(); 21 | const request = new RequestMock(); 22 | const url = '/foo/bar/folder'; 23 | const props = { 24 | '{urn:ietf:params:xml:ns:caldav}calendar-availability': 'VAVAILABILITY123' 25 | } 26 | 27 | const scheduleInbox = new ScheduleInbox(parent, request, url, props); 28 | expect(scheduleInbox).toEqual(expect.any(Calendar)) 29 | }); 30 | 31 | it('should inherit expose the property calendar-availability', () => { 32 | const parent = new DavCollectionMock(); 33 | const request = new RequestMock(); 34 | const url = '/foo/bar/folder'; 35 | const props = { 36 | '{urn:ietf:params:xml:ns:caldav}calendar-availability': 'VAVAILABILITY123' 37 | } 38 | 39 | const scheduleInbox = new ScheduleInbox(parent, request, url, props); 40 | expect(scheduleInbox.availability).toEqual('VAVAILABILITY123'); 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /test/unit/models/scheduleOutboxTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { assert, describe, expect, it, vi } from "vitest"; 11 | 12 | import ScheduleOutbox from "../../../src/models/scheduleOutbox.js"; 13 | import { DavCollection } from "../../../src/models/davCollection.js"; 14 | import RequestMock from "../../mocks/request.mock.js"; 15 | import { DavCollection as DavCollectionMock } from "../../mocks/davCollection.mock.js"; 16 | 17 | describe('Schedule outbox model', () => { 18 | 19 | it('should inherit from DavCollection', () => { 20 | const parent = new DavCollectionMock(); 21 | const request = new RequestMock(); 22 | const url = '/foo/bar/folder'; 23 | const props = {} 24 | 25 | const scheduleOutbox = new ScheduleOutbox(parent, request, url, props) 26 | expect(scheduleOutbox).toEqual(expect.any(DavCollection)) 27 | }); 28 | 29 | it('should provide a method to gather free/busy data', () => { 30 | const parent = new DavCollectionMock(); 31 | const request = new RequestMock(); 32 | const url = '/foo/bar/folder'; 33 | const props = {} 34 | 35 | const scheduleOutbox = new ScheduleOutbox(parent, request, url, props) 36 | 37 | const requestData = `BEGIN:VCALENDAR 38 | ... 39 | END:VCALENDAR`; 40 | 41 | // Example response taken from https://tools.ietf.org/html/rfc6638#appendix-B.5 42 | const response = ` 43 | 44 | 45 | 46 | mailto:wilfredo@example.com 47 | 48 | 2.0;Success 49 | BEGIN:VCALENDAR 50 | VERSION:2.0 51 | PRODID:-//Example Corp.//CalDAV Server//EN 52 | METHOD:REPLY 53 | BEGIN:VFREEBUSY 54 | UID:4FD3AD926350 55 | DTSTAMP:20090602T200733Z 56 | DTSTART:20090602T000000Z 57 | DTEND:20090604T000000Z 58 | ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com 59 | ATTENDEE;CN="Wilfredo Sanchez Vega":mailto:wilfredo@example.com 60 | FREEBUSY;FBTYPE=BUSY:20090602T110000Z/20090602T120000Z 61 | FREEBUSY;FBTYPE=BUSY:20090603T170000Z/20090603T180000Z 62 | END:VFREEBUSY 63 | END:VCALENDAR 64 | 65 | 66 | 67 | 68 | mailto:bernard@example.net 69 | 70 | 2.0;Success 71 | BEGIN:VCALENDAR 72 | VERSION:2.0 73 | PRODID:-//Example Corp.//CalDAV Server//EN 74 | METHOD:REPLY 75 | BEGIN:VFREEBUSY 76 | UID:4FD3AD926350 77 | DTSTAMP:20090602T200733Z 78 | DTSTART:20090602T000000Z 79 | DTEND:20090604T000000Z 80 | ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com 81 | ATTENDEE;CN="Bernard Desruisseaux":mailto:bernard@example.net 82 | FREEBUSY;FBTYPE=BUSY:20090602T150000Z/20090602T160000Z 83 | FREEBUSY;FBTYPE=BUSY:20090603T090000Z/20090603T100000Z 84 | FREEBUSY;FBTYPE=BUSY:20090603T180000Z/20090603T190000Z 85 | END:VFREEBUSY 86 | END:VCALENDAR 87 | 88 | 89 | 90 | 91 | mailto:mike@example.org 92 | 93 | 3.7;Invalid calendar user 94 | 95 | ` 96 | 97 | request.post.mockImplementation(() => { 98 | return Promise.resolve({ 99 | status: 207, 100 | body: response, 101 | headers: {} 102 | }) 103 | }); 104 | 105 | return scheduleOutbox.freeBusyRequest(requestData).then((freeBusyData) => { 106 | expect(freeBusyData).toEqual({ 107 | 'mailto:wilfredo@example.com': { 108 | calendarData: `BEGIN:VCALENDAR 109 | VERSION:2.0 110 | PRODID:-//Example Corp.//CalDAV Server//EN 111 | METHOD:REPLY 112 | BEGIN:VFREEBUSY 113 | UID:4FD3AD926350 114 | DTSTAMP:20090602T200733Z 115 | DTSTART:20090602T000000Z 116 | DTEND:20090604T000000Z 117 | ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com 118 | ATTENDEE;CN="Wilfredo Sanchez Vega":mailto:wilfredo@example.com 119 | FREEBUSY;FBTYPE=BUSY:20090602T110000Z/20090602T120000Z 120 | FREEBUSY;FBTYPE=BUSY:20090603T170000Z/20090603T180000Z 121 | END:VFREEBUSY 122 | END:VCALENDAR 123 | `, 124 | status: '2.0;Success', 125 | success: true 126 | }, 127 | 'mailto:bernard@example.net': { 128 | calendarData: `BEGIN:VCALENDAR 129 | VERSION:2.0 130 | PRODID:-//Example Corp.//CalDAV Server//EN 131 | METHOD:REPLY 132 | BEGIN:VFREEBUSY 133 | UID:4FD3AD926350 134 | DTSTAMP:20090602T200733Z 135 | DTSTART:20090602T000000Z 136 | DTEND:20090604T000000Z 137 | ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com 138 | ATTENDEE;CN="Bernard Desruisseaux":mailto:bernard@example.net 139 | FREEBUSY;FBTYPE=BUSY:20090602T150000Z/20090602T160000Z 140 | FREEBUSY;FBTYPE=BUSY:20090603T090000Z/20090603T100000Z 141 | FREEBUSY;FBTYPE=BUSY:20090603T180000Z/20090603T190000Z 142 | END:VFREEBUSY 143 | END:VCALENDAR 144 | `, 145 | status: '2.0;Success', 146 | success: true 147 | }, 148 | 'mailto:mike@example.org': { 149 | calendarData: '', 150 | status: '3.7;Invalid calendar user', 151 | success: false 152 | } 153 | }); 154 | 155 | expect(request.post).toHaveBeenCalledTimes(1) 156 | expect(request.post).toHaveBeenCalledWith('/foo/bar/folder/', { 157 | 'Content-Type': 'text/calendar; charset="utf-8"' 158 | }, requestData); 159 | }).catch(() => { 160 | assert.fail('Calendar findAllVObjects was not supposed to assert.fail'); 161 | }); 162 | 163 | }) 164 | 165 | }); 166 | -------------------------------------------------------------------------------- /test/unit/models/subscriptionTest.js: -------------------------------------------------------------------------------- 1 | import { describe } from "vitest"; 2 | /** 3 | * CDAV Library 4 | * 5 | * This library is part of the Nextcloud project 6 | * 7 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 8 | * SPDX-License-Identifier: AGPL-3.0-or-later 9 | */ 10 | 11 | describe.todo('Subscription model'); -------------------------------------------------------------------------------- /test/unit/models/vcardTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it, vi } from "vitest"; 11 | 12 | import { DavObject } from "../../../src/models/davObject.js"; 13 | import { VCard } from "../../../src/models/vcard.js"; 14 | import RequestMock from "../../mocks/request.mock.js"; 15 | import { DavCollection as DavCollectionMock } from "../../mocks/davCollection.mock.js"; 16 | 17 | describe('VCard model', () => { 18 | 19 | it('should inherit from DavObject', () => { 20 | const parent = new DavCollectionMock(); 21 | const request = new RequestMock(); 22 | const url = '/foo/bar/file'; 23 | const props = { 24 | '{DAV:}getetag': '"etag foo bar tralala"', 25 | '{DAV:}getcontenttype': 'text/blub', 26 | '{DAV:}resourcetype': [], 27 | '{urn:ietf:params:xml:ns:carddav}address-data': 'FOO BAR BLA BLUB', 28 | }; 29 | 30 | const vcard = new VCard(parent, request, url, props); 31 | expect(vcard).toEqual(expect.any(DavObject)); 32 | }); 33 | 34 | it('should expose the address-data as a property', () => { 35 | const parent = new DavCollectionMock(); 36 | const request = new RequestMock(); 37 | const url = '/foo/bar/file'; 38 | const props = { 39 | '{DAV:}getetag': '"etag foo bar tralala"', 40 | '{DAV:}getcontenttype': 'text/blub', 41 | '{DAV:}resourcetype': [], 42 | '{urn:ietf:params:xml:ns:carddav}address-data': 'FOO BAR BLA BLUB', 43 | }; 44 | 45 | const vcard = new VCard(parent, request, url, props); 46 | expect(vcard.data).toEqual('FOO BAR BLA BLUB'); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /test/unit/models/vobjectTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it } from "vitest"; 11 | 12 | import { DavObject } from "../../../src/models/davObject.js"; 13 | import { VObject } from "../../../src/models/vobject.js"; 14 | import RequestMock from "../../mocks/request.mock.js"; 15 | import { DavCollection as DavCollectionMock } from "../../mocks/davCollection.mock.js"; 16 | 17 | describe('VObject model', () => { 18 | 19 | it('should inherit from DavObject', () => { 20 | const parent = new DavCollectionMock(); 21 | const request = new RequestMock(); 22 | const url = '/foo/bar/file'; 23 | const props = { 24 | '{DAV:}getetag': '"etag foo bar tralala"', 25 | '{DAV:}getcontenttype': 'text/blub', 26 | '{DAV:}resourcetype': [], 27 | '{urn:ietf:params:xml:ns:caldav}calendar-data': 'FOO BAR BLA BLUB', 28 | }; 29 | 30 | const vobject = new VObject(parent, request, url, props); 31 | expect(vobject).toEqual(expect.any(DavObject)); 32 | }); 33 | 34 | it('should expose the calendar-data as a property', () => { 35 | const parent = new DavCollectionMock(); 36 | const request = new RequestMock(); 37 | const url = '/foo/bar/file'; 38 | const props = { 39 | '{DAV:}getetag': '"etag foo bar tralala"', 40 | '{DAV:}getcontenttype': 'text/blub', 41 | '{DAV:}resourcetype': [], 42 | '{urn:ietf:params:xml:ns:caldav}calendar-data': 'FOO BAR BLA BLUB', 43 | }; 44 | 45 | const vobject = new VObject(parent, request, url, props); 46 | expect(vobject.data).toEqual('FOO BAR BLA BLUB'); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /test/unit/propset/addressBookPropSetTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it } from "vitest"; 11 | 12 | import addressBookPropSet from "../../../src/propset/addressBookPropSet.js"; 13 | 14 | describe('Address book prop-set', () => { 15 | it('should ignore unknown properties', () => { 16 | expect(addressBookPropSet({ 17 | '{Foo:}bar': 123 18 | })).toEqual([]); 19 | }); 20 | 21 | it('should serialize {urn:ietf:params:xml:ns:carddav}addressbook-description correctly', () => { 22 | expect(addressBookPropSet({ 23 | '{Foo:}bar': 123, 24 | '{urn:ietf:params:xml:ns:carddav}addressbook-description': 'New addressbook description' 25 | })).toEqual([ 26 | { 27 | name: ['urn:ietf:params:xml:ns:carddav', 'addressbook-description'], 28 | value: 'New addressbook description' 29 | } 30 | ]); 31 | }); 32 | 33 | it('should serialize {http://owncloud.org/ns}enabled correctly - enabled', () => { 34 | expect(addressBookPropSet({ 35 | '{Foo:}bar': 123, 36 | '{http://owncloud.org/ns}enabled': true 37 | })).toEqual([ 38 | { 39 | name: ['http://owncloud.org/ns', 'enabled'], 40 | value: '1' 41 | } 42 | ]); 43 | }); 44 | 45 | it('should serialize {http://owncloud.org/ns}enabled correctly - disabled', () => { 46 | expect(addressBookPropSet({ 47 | '{Foo:}bar': 123, 48 | '{http://owncloud.org/ns}enabled': false 49 | })).toEqual([ 50 | { 51 | name: ['http://owncloud.org/ns', 'enabled'], 52 | value: '0' 53 | } 54 | ]); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/unit/propset/calendarPropSetTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it } from "vitest"; 11 | 12 | import calendarPropSet from "../../../src/propset/calendarPropSet.js"; 13 | 14 | describe('Calendar prop-set', () => { 15 | it('should ignore unknown properties', () => { 16 | expect(calendarPropSet({ 17 | '{Foo:}bar': 123 18 | })).toEqual([]); 19 | }); 20 | 21 | it('should serialize {http://apple.com/ns/ical/}calendar-order correctly', () => { 22 | expect(calendarPropSet({ 23 | '{Foo:}bar': 123, 24 | '{http://apple.com/ns/ical/}calendar-order': 4 25 | })).toEqual([ 26 | { 27 | name: ['http://apple.com/ns/ical/', 'calendar-order'], 28 | value: '4' 29 | } 30 | ]); 31 | }); 32 | 33 | it('should serialize {http://apple.com/ns/ical/}calendar-color correctly', () => { 34 | expect(calendarPropSet({ 35 | '{Foo:}bar': 123, 36 | '{http://apple.com/ns/ical/}calendar-color': '#AABBCC' 37 | })).toEqual([ 38 | { 39 | name: ['http://apple.com/ns/ical/', 'calendar-color'], 40 | value: '#AABBCC' 41 | } 42 | ]); 43 | }); 44 | 45 | it('should serialize {http://calendarserver.org/ns/}source correctly', () => { 46 | expect(calendarPropSet({ 47 | '{Foo:}bar': 123, 48 | '{http://calendarserver.org/ns/}source': 'http://foo.bar' 49 | })).toEqual([ 50 | { 51 | name: ['http://calendarserver.org/ns/', 'source'], 52 | children: [ 53 | { 54 | name: ['DAV:', 'href'], 55 | value: 'http://foo.bar' 56 | } 57 | ] 58 | } 59 | ]); 60 | }); 61 | 62 | it('should serialize {urn:ietf:params:xml:ns:caldav}calendar-description correctly', () => { 63 | expect(calendarPropSet({ 64 | '{Foo:}bar': 123, 65 | '{urn:ietf:params:xml:ns:caldav}calendar-description': 'New description for calendar' 66 | })).toEqual([ 67 | { 68 | name: ['urn:ietf:params:xml:ns:caldav', 'calendar-description'], 69 | value: 'New description for calendar' 70 | } 71 | ]); 72 | }); 73 | 74 | it('should serialize {urn:ietf:params:xml:ns:caldav}calendar-timezone correctly', () => { 75 | expect(calendarPropSet({ 76 | '{Foo:}bar': 123, 77 | '{urn:ietf:params:xml:ns:caldav}calendar-timezone': 'BEGIN:TIMEZONE...' 78 | })).toEqual([ 79 | { 80 | name: ['urn:ietf:params:xml:ns:caldav', 'calendar-timezone'], 81 | value: 'BEGIN:TIMEZONE...' 82 | } 83 | ]); 84 | }); 85 | 86 | it('should serialize {http://owncloud.org/ns}calendar-enabled correctly - enabled', () => { 87 | expect(calendarPropSet({ 88 | '{Foo:}bar': 123, 89 | '{http://owncloud.org/ns}calendar-enabled': true 90 | })).toEqual([ 91 | { 92 | name: ['http://owncloud.org/ns', 'calendar-enabled'], 93 | value: '1' 94 | } 95 | ]); 96 | }); 97 | 98 | it('should serialize {http://owncloud.org/ns}calendar-enabled correctly - disabled', () => { 99 | expect(calendarPropSet({ 100 | '{Foo:}bar': 123, 101 | '{http://owncloud.org/ns}calendar-enabled': false 102 | })).toEqual([ 103 | { 104 | name: ['http://owncloud.org/ns', 'calendar-enabled'], 105 | value: '0' 106 | } 107 | ]); 108 | }); 109 | 110 | it('should serialize {urn:ietf:params:xml:ns:caldav}schedule-calendar-transp correctly - transparent', () => { 111 | expect(calendarPropSet({ 112 | '{Foo:}bar': 123, 113 | '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': 'transparent' 114 | })).toEqual([ 115 | { 116 | name: ['urn:ietf:params:xml:ns:caldav', 'schedule-calendar-transp'], 117 | children: [ 118 | { 119 | name: ['urn:ietf:params:xml:ns:caldav', 'transparent'], 120 | }, 121 | ], 122 | } 123 | ]); 124 | }); 125 | 126 | it('should serialize {urn:ietf:params:xml:ns:caldav}schedule-calendar-transp correctly - opaque', () => { 127 | expect(calendarPropSet({ 128 | '{Foo:}bar': 123, 129 | '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': 'opaque' 130 | })).toEqual([ 131 | { 132 | name: ['urn:ietf:params:xml:ns:caldav', 'schedule-calendar-transp'], 133 | children: [ 134 | { 135 | name: ['urn:ietf:params:xml:ns:caldav', 'opaque'], 136 | }, 137 | ], 138 | } 139 | ]); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/unit/propset/davCollectionPropSetTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it } from "vitest"; 11 | 12 | import davCollectionPropSet from "../../../src/propset/davCollectionPropSet.js"; 13 | 14 | describe('Dav collection prop-set', () => { 15 | it('should ignore unknown properties', () => { 16 | expect(davCollectionPropSet({ 17 | '{Foo:}bar': 123 18 | })).toEqual([]); 19 | }); 20 | 21 | it('should serialize {DAV:}displayname correctly', () => { 22 | expect(davCollectionPropSet({ 23 | '{Foo:}bar': 123, 24 | '{DAV:}displayname': 'New displayname for collection' 25 | })).toEqual([ 26 | { 27 | name: ['DAV:', 'displayname'], 28 | value: 'New displayname for collection' 29 | } 30 | ]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/propset/principalPropSet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it } from "vitest"; 11 | 12 | import principalPropSet from '../../../src/propset/principalPropSet.js'; 13 | 14 | describe('Principal prop-set', () => { 15 | it('should ignore unknown properties', () => { 16 | expect(principalPropSet({ 17 | '{Foo:}bar': 123 18 | })).toEqual([]); 19 | }); 20 | 21 | it('should serialize {urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL correctly', () => { 22 | expect(principalPropSet({ 23 | '{Foo:}bar': 123, 24 | '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL': '/nextcloud/remote.php/dav/calendars/admin/personal/' 25 | })).toEqual([ 26 | { 27 | name: ['urn:ietf:params:xml:ns:caldav', 'schedule-default-calendar-URL'], 28 | children: [ 29 | { 30 | name: ['DAV:', 'href'], 31 | value: '/nextcloud/remote.php/dav/calendars/admin/personal/' 32 | } 33 | ] 34 | } 35 | ]); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/unit/propset/scheduleInboxPropSetTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it } from "vitest"; 11 | 12 | import scheduleInboxPropSet from "../../../src/propset/scheduleInboxPropSet.js"; 13 | 14 | describe('Schedule Inbox collection prop-set', () => { 15 | it('should ignore unknown properties', () => { 16 | expect(scheduleInboxPropSet({ 17 | '{Foo:}bar': 123 18 | })).toEqual([]); 19 | }); 20 | 21 | it('should serialize {DAV:}displayname correctly', () => { 22 | expect(scheduleInboxPropSet({ 23 | '{Foo:}bar': 123, 24 | '{urn:ietf:params:xml:ns:caldav}calendar-availability': 'NEW:VAVAILABILITY' 25 | })).toEqual([ 26 | { 27 | name: ['urn:ietf:params:xml:ns:caldav', 'calendar-availability'], 28 | value: 'NEW:VAVAILABILITY' 29 | } 30 | ]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/utility/namespaceUtilityTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it } from "vitest"; 11 | 12 | import * as NS from '../../../src/utility/namespaceUtility.js'; 13 | 14 | describe('NamespaceUtility', () => { 15 | it('should provide namespaces', () => { 16 | expect(NS.DAV).toEqual('DAV:'); 17 | expect(NS.IETF_CALDAV).toEqual('urn:ietf:params:xml:ns:caldav'); 18 | expect(NS.IETF_CARDDAV).toEqual('urn:ietf:params:xml:ns:carddav'); 19 | expect(NS.OWNCLOUD).toEqual('http://owncloud.org/ns'); 20 | expect(NS.NEXTCLOUD).toEqual('http://nextcloud.com/ns'); 21 | expect(NS.APPLE).toEqual('http://apple.com/ns/ical/'); 22 | expect(NS.CALENDARSERVER).toEqual('http://calendarserver.org/ns/'); 23 | expect(NS.SABREDAV).toEqual('http://sabredav.org/ns'); 24 | }); 25 | 26 | it('should provide namespace map', () => { 27 | expect(NS.NS_MAP).toEqual({ 28 | d: 'DAV:', 29 | cl: 'urn:ietf:params:xml:ns:caldav', 30 | cr: 'urn:ietf:params:xml:ns:carddav', 31 | oc: 'http://owncloud.org/ns', 32 | nc: 'http://nextcloud.com/ns', 33 | aapl: 'http://apple.com/ns/ical/', 34 | cs: 'http://calendarserver.org/ns/', 35 | sd: 'http://sabredav.org/ns' 36 | }); 37 | }); 38 | 39 | it ('should provide namespace resolver', () => { 40 | expect(NS.resolve('d')).toEqual('DAV:'); 41 | expect(NS.resolve('cl')).toEqual('urn:ietf:params:xml:ns:caldav'); 42 | expect(NS.resolve('cr')).toEqual('urn:ietf:params:xml:ns:carddav'); 43 | expect(NS.resolve('oc')).toEqual('http://owncloud.org/ns'); 44 | expect(NS.resolve('nc')).toEqual('http://nextcloud.com/ns'); 45 | expect(NS.resolve('aapl')).toEqual('http://apple.com/ns/ical/'); 46 | expect(NS.resolve('cs')).toEqual('http://calendarserver.org/ns/'); 47 | expect(NS.resolve('sd')).toEqual('http://sabredav.org/ns'); 48 | expect(NS.resolve('bliblablub')).toEqual(null); 49 | }); 50 | 51 | it('should only export namespaces', () => { 52 | expect(Object.keys(NS).length).toEqual(10); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/unit/utility/stringUtilityTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { describe, expect, it, vi } from "vitest"; 11 | 12 | import * as StringUtility from '../../../src/utility/stringUtility.js'; 13 | 14 | describe('StringUtility', () => { 15 | it('should return a unique identifier', function () { 16 | const uid = StringUtility.uid(); 17 | 18 | expect(uid).toEqual(expect.any(String)); 19 | expect(uid).toEqual(uid.toUpperCase()); 20 | }); 21 | 22 | it('should return a unique identifier with a prefix and/or a suffix', function () { 23 | const uid1 = StringUtility.uid('foobar'); 24 | 25 | expect(uid1).toEqual(expect.any(String)); 26 | expect(uid1.startsWith('foobar-')).toEqual(true); 27 | 28 | const uid2 = StringUtility.uid(null, 'ics'); 29 | 30 | expect(uid2).toEqual(expect.any(String)); 31 | expect(uid2.endsWith('.ics')).toEqual(true); 32 | 33 | const uid3 = StringUtility.uid('foobar', 'ics'); 34 | 35 | expect(uid3).toEqual(expect.any(String)); 36 | expect(uid3.startsWith('foobar-')).toEqual(true); 37 | expect(uid3.endsWith('.ics')).toEqual(true); 38 | }); 39 | 40 | it('should return the uri if it\'s available', function() { 41 | const isAvailable = vi.fn(() => true); 42 | const uri = StringUtility.uri('abc', isAvailable); 43 | 44 | expect(uri).toEqual('abc'); 45 | expect(isAvailable).toHaveBeenCalledWith('abc'); 46 | expect(isAvailable.mock.calls.length).toEqual(1); 47 | }); 48 | 49 | it('should not return an empty uri', function() { 50 | const isAvailable = vi.fn(() => true); 51 | const uri = StringUtility.uri('', isAvailable); 52 | 53 | expect(uri).toEqual('-'); 54 | expect(isAvailable).toHaveBeenCalledWith('-'); 55 | expect(isAvailable.mock.calls.length).toEqual(1); 56 | }); 57 | 58 | it('should be able to append -1 to the name', function() { 59 | const isAvailable = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true); 60 | const uri = StringUtility.uri('abc', isAvailable); 61 | 62 | expect(uri).toEqual('abc-1'); 63 | expect(isAvailable.mock.calls[0]).toEqual(['abc']); 64 | expect(isAvailable.mock.calls[1]).toEqual(['abc-1']); 65 | expect(isAvailable.mock.calls.length).toEqual(2); 66 | }); 67 | 68 | it('should be able to append 1 to the name if name contains - at the end', function() { 69 | const isAvailable = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true); 70 | const uri = StringUtility.uri('abc-', isAvailable); 71 | 72 | expect(uri).toEqual('abc-1'); 73 | expect(isAvailable.mock.calls[0]).toEqual(['abc']); 74 | expect(isAvailable.mock.calls[1]).toEqual(['abc-1']); 75 | expect(isAvailable.mock.calls.length).toEqual(2); 76 | }); 77 | 78 | it('should be able to append 1 to the name if name contains - in the middle', function() { 79 | const isAvailable = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true); 80 | const uri = StringUtility.uri('a-bc', isAvailable); 81 | 82 | expect(uri).toEqual('a-bc-1'); 83 | expect(isAvailable.mock.calls[0]).toEqual(['a-bc']); 84 | expect(isAvailable.mock.calls[1]).toEqual(['a-bc-1']); 85 | expect(isAvailable.mock.calls.length).toEqual(2); 86 | }); 87 | 88 | it('should be able to append number to the name if name contains - in the middle', function() { 89 | const isAvailable = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(false).mockReturnValueOnce(false).mockReturnValueOnce(false).mockReturnValueOnce(true); 90 | const uri = StringUtility.uri('a-bc', isAvailable); 91 | 92 | expect(uri).toEqual('a-bc-4'); 93 | expect(isAvailable.mock.calls[0]).toEqual(['a-bc']); 94 | expect(isAvailable.mock.calls[1]).toEqual(['a-bc-1']); 95 | expect(isAvailable.mock.calls[2]).toEqual(['a-bc-2']); 96 | expect(isAvailable.mock.calls[3]).toEqual(['a-bc-3']); 97 | expect(isAvailable.mock.calls[4]).toEqual(['a-bc-4']); 98 | expect(isAvailable.mock.calls.length).toEqual(5); 99 | }); 100 | 101 | it('should be lowercase', function() { 102 | const isAvailable = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true); 103 | const uri = StringUtility.uri('A-BC', isAvailable); 104 | 105 | expect(uri).toEqual('a-bc-1'); 106 | expect(isAvailable.mock.calls[0]).toEqual(['a-bc']); 107 | expect(isAvailable.mock.calls[1]).toEqual(['a-bc-1']); 108 | expect(isAvailable.mock.calls.length).toEqual(2); 109 | }); 110 | 111 | it('should work with emojis', function() { 112 | const isAvailable = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true); 113 | const uri = StringUtility.uri('💁🏼-123', isAvailable); 114 | 115 | expect(uri).toEqual('123-1'); 116 | expect(isAvailable.mock.calls[0]).toEqual(['123']); 117 | expect(isAvailable.mock.calls[1]).toEqual(['123-1']); 118 | expect(isAvailable.mock.calls.length).toEqual(2); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/unit/utility/xmlUtilityTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CDAV Library 3 | * 4 | * This library is part of the Nextcloud project 5 | * 6 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 7 | * SPDX-License-Identifier: AGPL-3.0-or-later 8 | */ 9 | 10 | import { beforeEach, describe, expect, it } from "vitest"; 11 | import { server } from '@vitest/browser/context'; 12 | 13 | import * as XMLUtility from '../../../src/utility/xmlUtility.js'; 14 | 15 | describe('XMLUtility', () => { 16 | beforeEach(() => { 17 | XMLUtility.resetPrefixMap(); 18 | }); 19 | 20 | it('should return an empty string when parameter is not an object', () => { 21 | expect(XMLUtility.serialize()).toEqual(''); 22 | expect(XMLUtility.serialize(null)).toEqual(''); 23 | expect(XMLUtility.serialize(123)).toEqual(''); 24 | expect(XMLUtility.serialize('abc')).toEqual(''); 25 | expect(XMLUtility.serialize([])).toEqual(''); 26 | expect(XMLUtility.serialize({})).toEqual(''); 27 | }); 28 | 29 | it('should return correct xml for one element', () => { 30 | expect(XMLUtility.serialize({ 31 | name: ['NS123', 'element'] 32 | })).toEqual(''); 33 | }); 34 | 35 | it('should return correct xml for one element with attributes', () => { 36 | expect(XMLUtility.serialize({ 37 | name: ['NS123', 'element'], 38 | attributes: [ 39 | ['abc', '123'], 40 | ['def', '456'] 41 | ] 42 | })).toEqual(''); 43 | }); 44 | 45 | it('should return correct xml for one element with namespaced attributes', () => { 46 | let expectedXml = '' 47 | if (server.browser === 'firefox') { 48 | // As of now, Firefox orders the XML attributes differently than Chromium and WebKit. 49 | // It uses the namespace `x1` in the attribute `x1:abc="123"` before declaring it with `xmlns:x1="myNs1"`. 50 | // It is legal XML but does not follow the specification of XML serialization described in https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-serializetostring 51 | // See https://bugzilla.mozilla.org/show_bug.cgi?id=1837472 52 | expectedXml = ''; 53 | } 54 | expect(XMLUtility.serialize({ 55 | name: ['NS123', 'element'], 56 | attributes: [ 57 | ['myNs1', 'abc', '123'], 58 | ['myNs2', 'def', '456'] 59 | ] 60 | })).toEqual(expectedXml); 61 | }); 62 | 63 | it('should return correct xml for one element with attributes and value', () => { 64 | expect(XMLUtility.serialize({ 65 | name: ['NS123', 'element'], 66 | attributes: [ 67 | ['abc', '123'], 68 | ['def', '456'] 69 | ], 70 | value: 'it value' 71 | })).toEqual('it value'); 72 | }); 73 | 74 | it('should prefer value over children', () => { 75 | expect(XMLUtility.serialize({ 76 | name: ['NS123', 'element'], 77 | attributes: [], 78 | value: 'it value', 79 | children: [{ 80 | name: 'element2' 81 | }] 82 | })).toEqual('it value'); 83 | }); 84 | 85 | it('should return correct xml for one child', () => { 86 | expect(XMLUtility.serialize({ 87 | name: ['NS123', 'element'], 88 | attributes: [ 89 | ['abc', '123'], 90 | ['def', '456'] 91 | ], 92 | children: [{ 93 | name: ['NS456', 'element2'] 94 | }] 95 | })).toEqual(''); 96 | }); 97 | 98 | it('should return correct xml for multiple children', () => { 99 | expect(XMLUtility.serialize({ 100 | name: ['NS123', 'element'], 101 | attributes: [ 102 | ['abc', '123'], 103 | ['def', '456'] 104 | ], 105 | children: [{ 106 | name: ['NS456', 'element'] 107 | }, { 108 | name: ['NS123', 'element2'] 109 | }] 110 | })).toEqual(''); 111 | }); 112 | 113 | it('should return correct xml for deeply nested objects', () => { 114 | expect(XMLUtility.serialize({ 115 | name: ['NSDAV', 'mkcol'], 116 | children: [{ 117 | name: ['NSDAV', 'set'], 118 | children: [{ 119 | name: ['NSDAV', 'prop'], 120 | children: [{ 121 | name: ['NSDAV', 'resourcetype'], 122 | children: [{ 123 | name: ['NSDAV', 'collection'], 124 | children: [{ 125 | name: ['NSCAL', 'calendar'] 126 | }] 127 | }, { 128 | name: ['NSDAV', 'displayname'], 129 | value: 'it_displayname' 130 | }, { 131 | name: ['NSOC', 'calendar-enabled'], 132 | value: 1 133 | }, { 134 | name: ['NSAAPL', 'calendar-order'], 135 | value: 42 136 | }, { 137 | name: ['NSAAPL', 'calendar-color'], 138 | value: '#00FF00' 139 | }, { 140 | name: ['NSCAL', 'supported-calendar-component-set'], 141 | children: [{ 142 | name: ['NSCAL', 'comp'], 143 | attributes: [ 144 | ['name', 'VEVENT'] 145 | ] 146 | },{ 147 | name: ['NSCAL', 'comp'], 148 | attributes: [ 149 | ['name', 'VTODO'] 150 | ] 151 | }] 152 | }] 153 | }] 154 | }] 155 | }] 156 | })).toEqual('it_displayname142#00FF00'); 157 | }); 158 | 159 | it('should return an empty object when getRootSkeleton is called with no parameters', () => { 160 | expect(XMLUtility.getRootSkeleton()).toEqual([{}, null]); 161 | }); 162 | 163 | it('should return the root sceleton correctly for one element', () => { 164 | const expected = { 165 | name: ['NSDAV', 'mkcol'], 166 | children: [] 167 | }; 168 | const result = XMLUtility.getRootSkeleton(['NSDAV', 'mkcol']); 169 | expect(result).toEqual([expected, expected.children]); 170 | expect(result[0].children === result[1]).toBe(true); 171 | }); 172 | 173 | it('should return the root sceleton correctly for two elements', () => { 174 | const expected = { 175 | name: ['NSDAV', 'mkcol'], 176 | children: [{ 177 | name: ['NSDAV', 'set'], 178 | children: [] 179 | }] 180 | }; 181 | const result = XMLUtility.getRootSkeleton(['NSDAV', 'mkcol'], 182 | ['NSDAV', 'set']); 183 | expect(result).toEqual([expected, expected.children[0].children]); 184 | expect(result[0].children[0].children === result[1]).toBe(true); 185 | }); 186 | 187 | it('should return the root sceleton correctly for three elements', () => { 188 | const expected = { 189 | name: ['NSDAV', 'mkcol'], 190 | children: [{ 191 | name: ['NSDAV', 'set'], 192 | children: [{ 193 | name: ['NSDAV', 'prop'], 194 | children: [] 195 | }] 196 | }] 197 | }; 198 | const result = XMLUtility.getRootSkeleton(['NSDAV', 'mkcol'], 199 | ['NSDAV', 'set'], ['NSDAV', 'prop']); 200 | expect(result).toEqual([expected, expected.children[0].children[0].children]); 201 | expect(result[0].children[0].children[0].children === result[1]).toBe(true); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { createLibConfig } from '@nextcloud/vite-config' 7 | 8 | export default createLibConfig({ 9 | index: 'src/index.js', 10 | }, { 11 | libraryFormats: ['es', 'cjs'], 12 | config: { 13 | test: { 14 | coverage: { 15 | include: ['src'], 16 | provider: 'istanbul', 17 | reporter: ['json'], 18 | reportOnFailure: true, 19 | }, 20 | restoreMocks: true, 21 | include: [ 22 | 'test/unit/**/*.js', 23 | ], 24 | browser: { 25 | enabled: true, 26 | screenshotFailures: false, 27 | headless: true, 28 | provider: 'playwright', 29 | instances: [ 30 | { browser: 'webkit' }, 31 | { browser: 'chromium' }, 32 | { browser: 'firefox' }, 33 | ], 34 | }, 35 | }, 36 | }, 37 | }) 38 | --------------------------------------------------------------------------------