From 587ffb9a20bda0dba081c4b723028145d798a6cb Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 13:24:03 +0800 Subject: [PATCH 01/18] Add forbidden filename-related capabilities Signed-off-by: Claudio Cambra --- Sources/NextcloudCapabilitiesKit/Files/Files.swift | 8 ++++++++ .../NextcloudCapabilitiesKitTests/Files/FilesTests.swift | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/Sources/NextcloudCapabilitiesKit/Files/Files.swift b/Sources/NextcloudCapabilitiesKit/Files/Files.swift index cf54324..75fe34f 100644 --- a/Sources/NextcloudCapabilitiesKit/Files/Files.swift +++ b/Sources/NextcloudCapabilitiesKit/Files/Files.swift @@ -6,6 +6,10 @@ import Foundation public struct Files: Equatable, Sendable { public let bigFileChunking: Bool public let blackListedFiles: [String] + public let forbiddenFilenames: [String] + public let forbiddenFilenameBasenames: [String] + public let forbiddenFilenameCharacters: [String] + public let forbiddenFilenameExtensions: [String] public let directEditing: DirectEditing? public let chunkedUpload: ChunkedUpload? public let comments: Bool @@ -23,6 +27,10 @@ public struct Files: Equatable, Sendable { bigFileChunking = capabilities["bigfilechunking"] as? Bool ?? false blackListedFiles = capabilities["blacklisted_files"] as? [String] ?? [] + forbiddenFilenames = capabilities["forbidden_filenames"] as? [String] ?? [] + forbiddenFilenameBasenames = capabilities["forbidden_filename_basenames"] as? [String] ?? [] + forbiddenFilenameCharacters = capabilities["forbidden_filename_characters"] as? [String] ?? [] + forbiddenFilenameExtensions = capabilities["forbidden_filename_extensions"] as? [String] ?? [] directEditing = DirectEditing(filesCapabilities: capabilities) chunkedUpload = ChunkedUpload(filesCapabilities: capabilities) comments = capabilities["comments"] as? Bool ?? false diff --git a/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift b/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift index 819ab6e..4d5e456 100644 --- a/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift @@ -11,6 +11,10 @@ class FilesTests: XCTestCase { "files": [ "bigfilechunking": true, "blacklisted_files": ["file1", "file2"], + "forbidden_filenames": ["forbidden"], + "forbidden_filename_basenames": [".DS_Store"], + "forbidden_filename_characters": ["/"], + "forbidden_filename_extensions": [".tmp"], "comments": false, "undelete": true, "versioning": false, @@ -34,6 +38,10 @@ class FilesTests: XCTestCase { XCTAssertNotNil(files, "Files instance should be created with valid input") XCTAssertEqual(files?.bigFileChunking, true, "Big File Chunking should be true") XCTAssertEqual(files?.blackListedFiles, ["file1", "file2"], "Blacklisted Files should match the provided values") + XCTAssertEqual(files?.forbiddenFilenames, ["forbidden"], "Forbidden filenames should match the provided values") + XCTAssertEqual(files?.forbiddenFilenameBasenames, [".DS_Store"], "Forbidden basenames should match the provided values") + XCTAssertEqual(files?.forbiddenFilenameCharacters, ["/"], "Forbidden characters should match the provided values") + XCTAssertEqual(files?.forbiddenFilenameExtensions, [".tmp"], "Forbidden extensions should match the provided values") XCTAssertNotNil(files?.directEditing, "Direct Editing should be initialized") XCTAssertEqual(files?.comments, false, "Comments should be false") XCTAssertEqual(files?.undelete, true, "Undelete should be true") @@ -71,6 +79,7 @@ class FilesTests: XCTestCase { XCTAssertNotNil(files, "Files instance should be created even with partially valid input") XCTAssertEqual(files?.bigFileChunking, true, "Big File Chunking should be true") XCTAssertEqual(files?.blackListedFiles, [], "Blacklisted Files should default to an empty array") + XCTAssertEqual(files?.forbiddenFilenames, [], "Forbidden filenames should default to an empty array") XCTAssertNil(files?.directEditing, "Direct Editing should be nil when not provided") XCTAssertEqual(files?.comments, false, "Comments should be false") XCTAssertEqual(files?.undelete, true, "Undelete should be true") From 95d40416f4643981cd6be1a2fa9f93bd627406c6 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 13:51:14 +0800 Subject: [PATCH 02/18] Add file conversions capabilities Signed-off-by: Claudio Cambra --- .../Files/FileConversion.swift | 26 +++++++++++++++++++ .../Files/Files.swift | 2 ++ .../Files/FilesTests.swift | 10 +++++++ 3 files changed, 38 insertions(+) create mode 100644 Sources/NextcloudCapabilitiesKit/Files/FileConversion.swift diff --git a/Sources/NextcloudCapabilitiesKit/Files/FileConversion.swift b/Sources/NextcloudCapabilitiesKit/Files/FileConversion.swift new file mode 100644 index 0000000..9efc906 --- /dev/null +++ b/Sources/NextcloudCapabilitiesKit/Files/FileConversion.swift @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +import Foundation + +public struct FileConversion: Equatable, Sendable { + public let from: String + public let to: String + public let `extension`: String + public let displayName: String + + init?(capabilities: [String: Any]) { + guard let from = capabilities["from"] as? String, + let to = capabilities["to"] as? String, + let fileExtension = capabilities["extension"] as? String, + let displayName = capabilities["displayName"] as? String + else { + return nil + } + + self.from = from + self.to = to + `extension` = fileExtension + self.displayName = displayName + } +} diff --git a/Sources/NextcloudCapabilitiesKit/Files/Files.swift b/Sources/NextcloudCapabilitiesKit/Files/Files.swift index 75fe34f..1ddbae8 100644 --- a/Sources/NextcloudCapabilitiesKit/Files/Files.swift +++ b/Sources/NextcloudCapabilitiesKit/Files/Files.swift @@ -10,6 +10,7 @@ public struct Files: Equatable, Sendable { public let forbiddenFilenameBasenames: [String] public let forbiddenFilenameCharacters: [String] public let forbiddenFilenameExtensions: [String] + public let fileConversions: [FileConversion] public let directEditing: DirectEditing? public let chunkedUpload: ChunkedUpload? public let comments: Bool @@ -31,6 +32,7 @@ public struct Files: Equatable, Sendable { forbiddenFilenameBasenames = capabilities["forbidden_filename_basenames"] as? [String] ?? [] forbiddenFilenameCharacters = capabilities["forbidden_filename_characters"] as? [String] ?? [] forbiddenFilenameExtensions = capabilities["forbidden_filename_extensions"] as? [String] ?? [] + fileConversions = (capabilities["file_conversions"] as? [[String: Any]] ?? []).compactMap(FileConversion.init) directEditing = DirectEditing(filesCapabilities: capabilities) chunkedUpload = ChunkedUpload(filesCapabilities: capabilities) comments = capabilities["comments"] as? Bool ?? false diff --git a/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift b/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift index 4d5e456..78da5d3 100644 --- a/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift @@ -15,6 +15,14 @@ class FilesTests: XCTestCase { "forbidden_filename_basenames": [".DS_Store"], "forbidden_filename_characters": ["/"], "forbidden_filename_extensions": [".tmp"], + "file_conversions": [ + [ + "from": "application/vnd.oasis.opendocument.text", + "to": "application/pdf", + "extension": "pdf", + "displayName": "PDF" + ] + ], "comments": false, "undelete": true, "versioning": false, @@ -42,6 +50,7 @@ class FilesTests: XCTestCase { XCTAssertEqual(files?.forbiddenFilenameBasenames, [".DS_Store"], "Forbidden basenames should match the provided values") XCTAssertEqual(files?.forbiddenFilenameCharacters, ["/"], "Forbidden characters should match the provided values") XCTAssertEqual(files?.forbiddenFilenameExtensions, [".tmp"], "Forbidden extensions should match the provided values") + XCTAssertEqual(files?.fileConversions.first?.extension, "pdf", "File conversion extension should match the provided value") XCTAssertNotNil(files?.directEditing, "Direct Editing should be initialized") XCTAssertEqual(files?.comments, false, "Comments should be false") XCTAssertEqual(files?.undelete, true, "Undelete should be true") @@ -80,6 +89,7 @@ class FilesTests: XCTestCase { XCTAssertEqual(files?.bigFileChunking, true, "Big File Chunking should be true") XCTAssertEqual(files?.blackListedFiles, [], "Blacklisted Files should default to an empty array") XCTAssertEqual(files?.forbiddenFilenames, [], "Forbidden filenames should default to an empty array") + XCTAssertEqual(files?.fileConversions, [], "File conversions should default to an empty array") XCTAssertNil(files?.directEditing, "Direct Editing should be nil when not provided") XCTAssertEqual(files?.comments, false, "Comments should be false") XCTAssertEqual(files?.undelete, true, "Undelete should be true") From 5bd91a7894c7c768556f163da124b45c17241c18 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 13:54:58 +0800 Subject: [PATCH 03/18] Add delete from trash capabilities Signed-off-by: Claudio Cambra --- Sources/NextcloudCapabilitiesKit/Files/Files.swift | 2 ++ Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Sources/NextcloudCapabilitiesKit/Files/Files.swift b/Sources/NextcloudCapabilitiesKit/Files/Files.swift index 1ddbae8..d8c5b6c 100644 --- a/Sources/NextcloudCapabilitiesKit/Files/Files.swift +++ b/Sources/NextcloudCapabilitiesKit/Files/Files.swift @@ -15,6 +15,7 @@ public struct Files: Equatable, Sendable { public let chunkedUpload: ChunkedUpload? public let comments: Bool public let undelete: Bool + public let deleteFromTrash: Bool public let versioning: Bool public let versionLabeling: Bool public let versionDeletion: Bool @@ -37,6 +38,7 @@ public struct Files: Equatable, Sendable { chunkedUpload = ChunkedUpload(filesCapabilities: capabilities) comments = capabilities["comments"] as? Bool ?? false undelete = capabilities["undelete"] as? Bool ?? false + deleteFromTrash = capabilities["delete_from_trash"] as? Bool ?? false versioning = capabilities["versioning"] as? Bool ?? false versionLabeling = capabilities["version_labeling"] as? Bool ?? false versionDeletion = capabilities["version_deletion"] as? Bool ?? false diff --git a/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift b/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift index 78da5d3..9dc1803 100644 --- a/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift @@ -25,6 +25,7 @@ class FilesTests: XCTestCase { ], "comments": false, "undelete": true, + "delete_from_trash": true, "versioning": false, "version_labeling": true, "version_deletion": false, @@ -54,6 +55,7 @@ class FilesTests: XCTestCase { XCTAssertNotNil(files?.directEditing, "Direct Editing should be initialized") XCTAssertEqual(files?.comments, false, "Comments should be false") XCTAssertEqual(files?.undelete, true, "Undelete should be true") + XCTAssertEqual(files?.deleteFromTrash, true, "Delete from trash should be true") XCTAssertEqual(files?.versioning, false, "Versioning should be false") XCTAssertEqual(files?.versionLabeling, true, "Version Labeling should be true") XCTAssertEqual(files?.versionDeletion, false, "Version Deletion should be false") @@ -98,5 +100,6 @@ class FilesTests: XCTestCase { XCTAssertEqual(files?.versionDeletion, false, "Version Deletion should be false") XCTAssertNil(files?.chunkedUpload, "Chunked Upload should be nil when not provided") XCTAssertNil(files?.locking, "Locking should be nil when not provided") + XCTAssertFalse(files?.deleteFromTrash ?? true, "Delete from trash should default to false when not provided") } } From 3ab2355846818348ede0dda0a112eaf044d93b5f Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 13:55:40 +0800 Subject: [PATCH 04/18] Add windows-compatible filenames capabilities Signed-off-by: Claudio Cambra --- Sources/NextcloudCapabilitiesKit/Files/Files.swift | 2 ++ Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/NextcloudCapabilitiesKit/Files/Files.swift b/Sources/NextcloudCapabilitiesKit/Files/Files.swift index d8c5b6c..83aa6bd 100644 --- a/Sources/NextcloudCapabilitiesKit/Files/Files.swift +++ b/Sources/NextcloudCapabilitiesKit/Files/Files.swift @@ -20,6 +20,7 @@ public struct Files: Equatable, Sendable { public let versionLabeling: Bool public let versionDeletion: Bool public let locking: String? + public let windowsCompatibleFilenames: Bool init?(capabilities: [String: Any]) { guard let capabilities = capabilities["files"] as? [String: Any] else { @@ -43,5 +44,6 @@ public struct Files: Equatable, Sendable { versionLabeling = capabilities["version_labeling"] as? Bool ?? false versionDeletion = capabilities["version_deletion"] as? Bool ?? false locking = capabilities["locking"] as? String ?? nil + windowsCompatibleFilenames = capabilities["windows_compatible_filenames"] as? Bool ?? false } } diff --git a/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift b/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift index 9dc1803..97259f5 100644 --- a/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/Files/FilesTests.swift @@ -38,7 +38,8 @@ class FilesTests: XCTestCase { "max_size": 100_000_000, "max_parallel_count": 5 ], - "locking": "1.0" + "locking": "1.0", + "windows_compatible_filenames": true ] ] @@ -63,6 +64,7 @@ class FilesTests: XCTestCase { XCTAssertEqual(files?.chunkedUpload?.maxChunkSize, 100_000_000) XCTAssertEqual(files?.chunkedUpload?.maxParallelCount, 5) XCTAssertEqual(files?.locking, "1.0", "Locking should match the provided value") + XCTAssertEqual(files?.windowsCompatibleFilenames, true, "Windows compatible filenames should be true") } func testInvalidFilesInitialization() { @@ -101,5 +103,6 @@ class FilesTests: XCTestCase { XCTAssertNil(files?.chunkedUpload, "Chunked Upload should be nil when not provided") XCTAssertNil(files?.locking, "Locking should be nil when not provided") XCTAssertFalse(files?.deleteFromTrash ?? true, "Delete from trash should default to false when not provided") + XCTAssertFalse(files?.windowsCompatibleFilenames ?? true, "Windows filename support should default to false when not provided") } } From 85bea26817c205915c52e1b1e1422e326abb5b92 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 13:56:43 +0800 Subject: [PATCH 05/18] Fix visibility issues in share types Signed-off-by: Claudio Cambra --- .../Shares/Share Type/Federation.swift | 8 ++++---- .../Shares/Share Type/Group.swift | 4 ++-- .../NextcloudCapabilitiesKit/Shares/Share Type/User.swift | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Federation.swift b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Federation.swift index 5644931..b22f868 100644 --- a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Federation.swift +++ b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Federation.swift @@ -4,10 +4,10 @@ import Foundation public struct Federation: Equatable, Sendable { - let outgoing: Bool - let incoming: Bool - let expireDateEnabled: Bool - let expireDateSupported: Bool + public let outgoing: Bool + public let incoming: Bool + public let expireDateEnabled: Bool + public let expireDateSupported: Bool init?(filesSharingCapabilities: [String: Any]) { guard let federationCaps = filesSharingCapabilities["federation"] as? [String: Any] else { diff --git a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Group.swift b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Group.swift index fec1dd4..6fd9a9f 100644 --- a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Group.swift +++ b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Group.swift @@ -4,8 +4,8 @@ import Foundation public struct Group: Equatable, Sendable { - let enabled: Bool - let expireDateEnabled: Bool + public let enabled: Bool + public let expireDateEnabled: Bool init?(filesSharingCapabilities: [String: Any]) { guard let groupCaps = filesSharingCapabilities["group"] as? [String: Any] else { diff --git a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/User.swift b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/User.swift index f1969f9..84ab4dc 100644 --- a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/User.swift +++ b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/User.swift @@ -4,8 +4,8 @@ import Foundation public struct User: Equatable, Sendable { - let sendMail: Bool - let expireDateEnabled: Bool + public let sendMail: Bool + public let expireDateEnabled: Bool init?(filesSharingCapabilities: [String: Any]) { guard let userCaps = filesSharingCapabilities["user"] as? [String: Any] else { From 31c22a078c6a93f5f72446d3a1dda3e70150d26e Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 13:57:52 +0800 Subject: [PATCH 06/18] Add missing properties to Email share capabilities Signed-off-by: Claudio Cambra --- .../Shares/Share Type/Email.swift | 17 +++++++++++++++++ .../Shares/Share Type/EmailTests.swift | 14 ++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Email.swift b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Email.swift index d69ac28..57f605e 100644 --- a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Email.swift +++ b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/Email.swift @@ -4,8 +4,13 @@ import Foundation public struct Email: Equatable, Sendable { + public let enabled: Bool + public let sendsPasswordByEmail: Bool + public let uploadFilesDropEnabled: Bool public let passwordEnabled: Bool public let passwordEnforced: Bool + public let expireDateEnabled: Bool + public let expireDateEnforced: Bool init?(filesSharingCapabilities: [String: Any]) { guard let emailCaps = filesSharingCapabilities["sharebymail"] as? [String: Any] else { @@ -13,6 +18,10 @@ public struct Email: Equatable, Sendable { return nil } + enabled = emailCaps["enabled"] as? Bool ?? false + sendsPasswordByEmail = emailCaps["send_password_by_mail"] as? Bool ?? false + uploadFilesDropEnabled = (emailCaps["upload_files_drop"] as? [String: Any])?["enabled"] as? Bool ?? false + if let passwordCapabilities = emailCaps["password"] as? [String: Any] { passwordEnabled = passwordCapabilities["enabled"] as? Bool ?? false passwordEnforced = passwordCapabilities["enforced"] as? Bool ?? false @@ -20,5 +29,13 @@ public struct Email: Equatable, Sendable { passwordEnabled = false passwordEnforced = false } + + if let expireDateCapabilities = emailCaps["expire_date"] as? [String: Any] { + expireDateEnabled = expireDateCapabilities["enabled"] as? Bool ?? false + expireDateEnforced = expireDateCapabilities["enforced"] as? Bool ?? false + } else { + expireDateEnabled = false + expireDateEnforced = false + } } } diff --git a/Tests/NextcloudCapabilitiesKitTests/Shares/Share Type/EmailTests.swift b/Tests/NextcloudCapabilitiesKitTests/Shares/Share Type/EmailTests.swift index 1495c71..4353452 100644 --- a/Tests/NextcloudCapabilitiesKitTests/Shares/Share Type/EmailTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/Shares/Share Type/EmailTests.swift @@ -9,17 +9,31 @@ final class EmailTests: XCTestCase { // Prepare test data let filesSharingCapabilities: [String: Any] = [ "sharebymail": [ + "enabled": true, + "send_password_by_mail": true, + "upload_files_drop": [ + "enabled": true + ], "password": [ "enabled": true, "enforced": false + ], + "expire_date": [ + "enabled": true, + "enforced": true ] ] ] let email = Email(filesSharingCapabilities: filesSharingCapabilities) XCTAssertNotNil(email) + XCTAssertEqual(email?.enabled, true) + XCTAssertEqual(email?.sendsPasswordByEmail, true) + XCTAssertEqual(email?.uploadFilesDropEnabled, true) XCTAssertEqual(email?.passwordEnabled, true) XCTAssertEqual(email?.passwordEnforced, false) + XCTAssertEqual(email?.expireDateEnabled, true) + XCTAssertEqual(email?.expireDateEnforced, true) } func testEmailInitializationWithMissingData() { From 0a104ff7bc2ecfe79bb87abcee3e635fb3e61329 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 13:58:09 +0800 Subject: [PATCH 07/18] Add missing proprties to Public Link share capabilities Signed-off-by: Claudio Cambra --- .../Shares/Share Type/PublicLink.swift | 21 +++++++++++++++++-- .../Shares/Share Type/PublicLinkTests.swift | 14 +++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/PublicLink.swift b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/PublicLink.swift index 6132946..c045cfc 100644 --- a/Sources/NextcloudCapabilitiesKit/Shares/Share Type/PublicLink.swift +++ b/Sources/NextcloudCapabilitiesKit/Shares/Share Type/PublicLink.swift @@ -7,12 +7,17 @@ public struct PublicLink: Equatable, Sendable { public let enabled: Bool public let allowUpload: Bool public let supportsUploadOnly: Bool + public let sendMail: Bool + public let customTokens: Bool public let askOptionalPassword: Bool public let passwordEnforced: Bool + public let expireDateEnabled: Bool public let expireDateEnforced: Bool public let expireDateDays: Int + public let internalExpireDateEnabled: Bool public let internalExpireDateEnforced: Bool public let internalExpireDateDays: Int + public let remoteExpireDateEnabled: Bool public let remoteExpireDateEnforced: Bool public let remoteExpireDateDays: Int public let multipleAllowed: Bool @@ -25,8 +30,14 @@ public struct PublicLink: Equatable, Sendable { enabled = publicLinkCaps["enabled"] as? Bool ?? false allowUpload = publicLinkCaps["upload"] as? Bool ?? false - supportsUploadOnly = publicLinkCaps["supports_upload_only"] as? Bool ?? false - multipleAllowed = publicLinkCaps["multiple"] as? Bool ?? false + supportsUploadOnly = publicLinkCaps["upload_files_drop"] as? Bool + ?? publicLinkCaps["supports_upload_only"] as? Bool + ?? false + sendMail = publicLinkCaps["send_mail"] as? Bool ?? false + customTokens = publicLinkCaps["custom_tokens"] as? Bool ?? false + multipleAllowed = publicLinkCaps["multiple_links"] as? Bool + ?? publicLinkCaps["multiple"] as? Bool + ?? false if let passwordCaps = publicLinkCaps["password"] as? [String: Any] { askOptionalPassword = passwordCaps["askForOptionalPassword"] as? Bool ?? false @@ -37,25 +48,31 @@ public struct PublicLink: Equatable, Sendable { } if let expireDateCapabilities = publicLinkCaps["expire_date"] as? [String: Any] { + expireDateEnabled = expireDateCapabilities["enabled"] as? Bool ?? false expireDateDays = expireDateCapabilities["days"] as? Int ?? 1 expireDateEnforced = expireDateCapabilities["enforced"] as? Bool ?? false } else { + expireDateEnabled = false expireDateDays = 1 expireDateEnforced = false } if let internalExpDateCaps = publicLinkCaps["expire_date_internal"] as? [String: Any] { + internalExpireDateEnabled = internalExpDateCaps["enabled"] as? Bool ?? false internalExpireDateDays = internalExpDateCaps["days"] as? Int ?? 1 internalExpireDateEnforced = internalExpDateCaps["enforced"] as? Bool ?? false } else { + internalExpireDateEnabled = false internalExpireDateDays = 1 internalExpireDateEnforced = false } if let remoteExpDateCaps = publicLinkCaps["expire_date_remote"] as? [String: Any] { + remoteExpireDateEnabled = remoteExpDateCaps["enabled"] as? Bool ?? false remoteExpireDateDays = remoteExpDateCaps["days"] as? Int ?? 1 remoteExpireDateEnforced = remoteExpDateCaps["enforced"] as? Bool ?? false } else { + remoteExpireDateEnabled = false remoteExpireDateDays = 1 remoteExpireDateEnforced = false } diff --git a/Tests/NextcloudCapabilitiesKitTests/Shares/Share Type/PublicLinkTests.swift b/Tests/NextcloudCapabilitiesKitTests/Shares/Share Type/PublicLinkTests.swift index 54912f0..25c001c 100644 --- a/Tests/NextcloudCapabilitiesKitTests/Shares/Share Type/PublicLinkTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/Shares/Share Type/PublicLinkTests.swift @@ -10,21 +10,26 @@ final class PublicLinkTests: XCTestCase { "public": [ "enabled": true, "upload": false, - "supports_upload_only": true, - "multiple": false, + "upload_files_drop": true, + "send_mail": true, + "custom_tokens": true, + "multiple_links": false, "password": [ "askForOptionalPassword": true, "enforced": false ], "expire_date": [ + "enabled": true, "days": 10, "enforced": true ], "expire_date_internal": [ + "enabled": true, "days": 5, "enforced": true ], "expire_date_remote": [ + "enabled": true, "days": 7, "enforced": false ] @@ -37,13 +42,18 @@ final class PublicLinkTests: XCTestCase { XCTAssertEqual(publicLink?.enabled, true, "Enabled should be true") XCTAssertEqual(publicLink?.allowUpload, false, "AllowUpload should be false") XCTAssertEqual(publicLink?.supportsUploadOnly, true, "SupportsUploadOnly should be true") + XCTAssertEqual(publicLink?.sendMail, true, "SendMail should be true") + XCTAssertEqual(publicLink?.customTokens, true, "CustomTokens should be true") XCTAssertEqual(publicLink?.askOptionalPassword, true, "AskOptionalPassword should be true") XCTAssertEqual(publicLink?.passwordEnforced, false, "PasswordEnforced should be false") XCTAssertEqual(publicLink?.expireDateEnforced, true, "ExpireDateEnforced should be true") + XCTAssertEqual(publicLink?.expireDateEnabled, true, "ExpireDateEnabled should be true") XCTAssertEqual(publicLink?.expireDateDays, 10, "ExpireDateDays should be 10") XCTAssertEqual(publicLink?.internalExpireDateEnforced, true, "InternalExpireDateEnforced should be true") + XCTAssertEqual(publicLink?.internalExpireDateEnabled, true, "InternalExpireDateEnabled should be true") XCTAssertEqual(publicLink?.internalExpireDateDays, 5, "InternalExpireDateDays should be 5") XCTAssertEqual(publicLink?.remoteExpireDateEnforced, false, "RemoteExpireDateEnforced should be false") + XCTAssertEqual(publicLink?.remoteExpireDateEnabled, true, "RemoteExpireDateEnabled should be true") XCTAssertEqual(publicLink?.remoteExpireDateDays, 7, "RemoteExpireDateDays should be 7") XCTAssertEqual(publicLink?.multipleAllowed, false, "MultipleAllowed should be false") } From 3dc24bef39ddd376da5d8c50a2c3d41b7db66fd2 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 13:58:29 +0800 Subject: [PATCH 08/18] Update FilesSharing capabilities Signed-off-by: Claudio Cambra --- .../Shares/FilesSharing.swift | 4 +++ .../Shares/FilesSharingTests.swift | 30 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudCapabilitiesKit/Shares/FilesSharing.swift b/Sources/NextcloudCapabilitiesKit/Shares/FilesSharing.swift index 56d452d..bb7d792 100644 --- a/Sources/NextcloudCapabilitiesKit/Shares/FilesSharing.swift +++ b/Sources/NextcloudCapabilitiesKit/Shares/FilesSharing.swift @@ -12,7 +12,9 @@ public struct FilesSharing: Equatable, Sendable { public let sharee: Sharee? public let apiEnabled: Bool public let resharing: Bool + public let groupSharing: Bool public let defaultPermissions: Int + public let excludeReshareFromEdit: Bool init?(capabilities: [String: Any]) { guard let capabilities = capabilities["files_sharing"] as? [String: Any] else { @@ -28,7 +30,9 @@ public struct FilesSharing: Equatable, Sendable { sharee = Sharee(filesSharingCapabilities: capabilities) apiEnabled = capabilities["api_enabled"] as? Bool ?? false resharing = capabilities["resharing"] as? Bool ?? false + groupSharing = capabilities["group_sharing"] as? Bool ?? false defaultPermissions = capabilities["default_permissions"] as? Int ?? 0 + excludeReshareFromEdit = capabilities["exclude_reshare_from_edit"] as? Bool ?? false debugPrint("Parsed share capabilities.") } } diff --git a/Tests/NextcloudCapabilitiesKitTests/Shares/FilesSharingTests.swift b/Tests/NextcloudCapabilitiesKitTests/Shares/FilesSharingTests.swift index c3bdf24..09789e5 100644 --- a/Tests/NextcloudCapabilitiesKitTests/Shares/FilesSharingTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/Shares/FilesSharingTests.swift @@ -10,29 +10,43 @@ class FilesSharingTests: XCTestCase { let validCapabilities: [String: Any] = [ "files_sharing": [ "sharebymail": [ + "enabled": true, + "send_password_by_mail": true, + "upload_files_drop": [ + "enabled": true + ], "password": [ "enabled": true, "enforced": false + ], + "expire_date": [ + "enabled": true, + "enforced": true ] ], "public": [ "enabled": true, "upload": false, - "supports_upload_only": true, - "multiple": false, + "upload_files_drop": true, + "send_mail": true, + "custom_tokens": true, + "multiple_links": false, "password": [ "askForOptionalPassword": true, "enforced": false ], "expire_date": [ + "enabled": true, "days": 10, "enforced": true ], "expire_date_internal": [ + "enabled": true, "days": 5, "enforced": true ], "expire_date_remote": [ + "enabled": true, "days": 7, "enforced": false ] @@ -60,12 +74,15 @@ class FilesSharingTests: XCTestCase { ] ], "sharee": [ + "minSearchStringLength": 3, "query_lookup_default": true, "always_show_unique": false ], "api_enabled": true, "resharing": false, - "default_permissions": 31 + "group_sharing": true, + "default_permissions": 31, + "exclude_reshare_from_edit": true ] ] @@ -79,7 +96,12 @@ class FilesSharingTests: XCTestCase { XCTAssertNotNil(filesSharing?.sharee, "Sharee subobject should be initialized") XCTAssertEqual(filesSharing?.apiEnabled, true, "APIEnabled should be true") XCTAssertEqual(filesSharing?.resharing, false, "Resharing should be false") - XCTAssertEqual(filesSharing?.defaultPermissions, 31, "DefaultPermissions should be 755") + XCTAssertEqual(filesSharing?.groupSharing, true, "Group sharing should be true") + XCTAssertEqual(filesSharing?.defaultPermissions, 31, "DefaultPermissions should be 31") + XCTAssertEqual(filesSharing?.excludeReshareFromEdit, true, "Exclude reshare from edit should be true") + XCTAssertEqual(filesSharing?.email?.sendsPasswordByEmail, true, "Share by mail should send passwords") + XCTAssertEqual(filesSharing?.publicLink?.customTokens, true, "Public links should support custom tokens") + XCTAssertEqual(filesSharing?.sharee?.minSearchStringLength, 3, "Sharee search length should match the provided value") } func testInvalidFilesSharingInitialization() { From 76882e4b232ae9c3eef402f3cb6fdec85f0b651b Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 13:58:46 +0800 Subject: [PATCH 09/18] Update Sharee Signed-off-by: Claudio Cambra --- Sources/NextcloudCapabilitiesKit/Shares/Sharee.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudCapabilitiesKit/Shares/Sharee.swift b/Sources/NextcloudCapabilitiesKit/Shares/Sharee.swift index 601e72e..1e5c6f8 100644 --- a/Sources/NextcloudCapabilitiesKit/Shares/Sharee.swift +++ b/Sources/NextcloudCapabilitiesKit/Shares/Sharee.swift @@ -4,8 +4,9 @@ import Foundation public struct Sharee: Equatable, Sendable { - let queryLookupDefault: Bool - let alwaysShowUnique: Bool + public let minSearchStringLength: Int + public let queryLookupDefault: Bool + public let alwaysShowUnique: Bool init?(filesSharingCapabilities: [String: Any]) { guard let shareeCaps = filesSharingCapabilities["sharee"] as? [String: Any] else { @@ -13,6 +14,7 @@ public struct Sharee: Equatable, Sendable { return nil } + minSearchStringLength = shareeCaps["minSearchStringLength"] as? Int ?? 0 queryLookupDefault = shareeCaps["query_lookup_default"] as? Bool ?? false alwaysShowUnique = shareeCaps["always_show_unique"] as? Bool ?? false } From d7a0ca8144340dc7ff185a12a0e89bcb6668d071 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 14:02:20 +0800 Subject: [PATCH 10/18] Update Theming Signed-off-by: Claudio Cambra --- .../NextcloudCapabilitiesKit/Theming.swift | 73 ++++++++++++++----- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/Sources/NextcloudCapabilitiesKit/Theming.swift b/Sources/NextcloudCapabilitiesKit/Theming.swift index 08d2542..fdffe63 100644 --- a/Sources/NextcloudCapabilitiesKit/Theming.swift +++ b/Sources/NextcloudCapabilitiesKit/Theming.swift @@ -4,20 +4,46 @@ import Foundation public struct Theming: Equatable, Sendable { - let name: String - let url: URL? - let slogan: String - let color: String - let colorText: String - let colorElement: String - let colorElementBright: String - let colorElementDark: String - let logoUrl: URL? - let backgroundColor: String - let backgroundPlain: Bool - let backgroundDefault: Bool - let logoHeaderUrl: URL? - let faviconUrl: URL? + public let name: String + public let productName: String + public let url: URL? + public let imprintURL: URL? + public let privacyURL: URL? + public let slogan: String + public let color: String + public let colorText: String + public let colorElement: String + public let colorElementBright: String + public let colorElementDark: String + public let logoURL: URL? + public let background: String + public let backgroundText: String + public let backgroundPlain: Bool + public let backgroundDefault: Bool + public let logoHeaderURL: URL? + public let faviconURL: URL? + public let primaryColor: String + public let backgroundColor: String + public let defaultPrimaryColor: String + public let defaultBackgroundColor: String + public let inverted: Bool + public let cacheBuster: String + public let enabledThemes: [String] + + @available(*, deprecated, renamed: "logoURL") + public var logoUrl: URL? { + logoURL + } + + @available(*, deprecated, renamed: "logoHeaderURL") + public var logoHeaderUrl: URL? { + logoHeaderURL + } + + @available(*, deprecated, renamed: "faviconURL") + public var faviconUrl: URL? { + faviconURL + } init?(capabilities: [String: Any]) { guard let capabilities = capabilities["theming"] as? [String: Any] else { @@ -26,18 +52,29 @@ public struct Theming: Equatable, Sendable { } name = capabilities["name"] as? String ?? "" + productName = capabilities["productName"] as? String ?? "" url = URL(string: capabilities["url"] as? String ?? "") + imprintURL = URL(string: capabilities["imprintUrl"] as? String ?? "") + privacyURL = URL(string: capabilities["privacyUrl"] as? String ?? "") slogan = capabilities["slogan"] as? String ?? "" color = capabilities["color"] as? String ?? "" colorText = capabilities["color-text"] as? String ?? "" colorElement = capabilities["color-element"] as? String ?? "" colorElementBright = capabilities["color-element-bright"] as? String ?? "" colorElementDark = capabilities["color-element-dark"] as? String ?? "" - logoUrl = URL(string: capabilities["logo"] as? String ?? "") - backgroundColor = capabilities["background"] as? String ?? "" + logoURL = URL(string: capabilities["logo"] as? String ?? "") + background = capabilities["background"] as? String ?? "" + backgroundText = capabilities["background-text"] as? String ?? "" backgroundPlain = capabilities["background-plain"] as? Bool ?? false backgroundDefault = capabilities["background-default"] as? Bool ?? false - logoHeaderUrl = URL(string: capabilities["logoheader"] as? String ?? "") - faviconUrl = URL(string: capabilities["favicon"] as? String ?? "") + logoHeaderURL = URL(string: capabilities["logoheader"] as? String ?? "") + faviconURL = URL(string: capabilities["favicon"] as? String ?? "") + primaryColor = capabilities["primaryColor"] as? String ?? "" + backgroundColor = capabilities["backgroundColor"] as? String ?? background + defaultPrimaryColor = capabilities["defaultPrimaryColor"] as? String ?? "" + defaultBackgroundColor = capabilities["defaultBackgroundColor"] as? String ?? "" + inverted = capabilities["inverted"] as? Bool ?? false + cacheBuster = capabilities["cacheBuster"] as? String ?? "" + enabledThemes = capabilities["enabledThemes"] as? [String] ?? [] } } From 75790c9a2f42ec98e8626e385df073691159b713 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 14:02:43 +0800 Subject: [PATCH 11/18] Update Dav Signed-off-by: Claudio Cambra --- Sources/NextcloudCapabilitiesKit/Dav.swift | 18 +++++++++++++++--- .../DavTests.swift | 16 ++++++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Sources/NextcloudCapabilitiesKit/Dav.swift b/Sources/NextcloudCapabilitiesKit/Dav.swift index 6607543..a7ec506 100644 --- a/Sources/NextcloudCapabilitiesKit/Dav.swift +++ b/Sources/NextcloudCapabilitiesKit/Dav.swift @@ -4,8 +4,14 @@ import Foundation public struct Dav: Equatable, Sendable { - let chunking: String - let bulkUpload: String + public let chunking: String + public let publicSharesChunking: Bool + public let searchSupportsCreationTime: Bool + public let searchSupportsUploadTime: Bool + public let searchSupportsLastActivity: Bool + public let bulkUpload: String? + public let absenceSupported: Bool + public let absenceReplacement: Bool init?(capabilities: [String: Any]) { guard let capabilities = capabilities["dav"] as? [String: Any] else { @@ -14,6 +20,12 @@ public struct Dav: Equatable, Sendable { } chunking = capabilities["chunking"] as? String ?? "" - bulkUpload = capabilities["bulkupload"] as? String ?? "" + publicSharesChunking = capabilities["public_shares_chunking"] as? Bool ?? false + searchSupportsCreationTime = capabilities["search_supports_creation_time"] as? Bool ?? false + searchSupportsUploadTime = capabilities["search_supports_upload_time"] as? Bool ?? false + searchSupportsLastActivity = capabilities["search_supports_last_activity"] as? Bool ?? false + bulkUpload = capabilities["bulkupload"] as? String + absenceSupported = capabilities["absence-supported"] as? Bool ?? false + absenceReplacement = capabilities["absence-replacement"] as? Bool ?? false } } diff --git a/Tests/NextcloudCapabilitiesKitTests/DavTests.swift b/Tests/NextcloudCapabilitiesKitTests/DavTests.swift index 556e736..e7bd466 100644 --- a/Tests/NextcloudCapabilitiesKitTests/DavTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/DavTests.swift @@ -10,14 +10,26 @@ class DavTests: XCTestCase { let capabilities: [String: Any] = [ "dav": [ "chunking": "1.0", - "bulkupload": "1.0" + "public_shares_chunking": true, + "search_supports_creation_time": true, + "search_supports_upload_time": true, + "search_supports_last_activity": true, + "bulkupload": "1.0", + "absence-supported": true, + "absence-replacement": true ] ] let dav = Dav(capabilities: capabilities) XCTAssertNotNil(dav, "Dav instance should be created with valid input") XCTAssertEqual(dav?.chunking, "1.0", "Dav chunking should match the provided value") + XCTAssertEqual(dav?.publicSharesChunking, true, "Dav public shares chunking should match the provided value") + XCTAssertEqual(dav?.searchSupportsCreationTime, true, "Dav creation time search should match the provided value") + XCTAssertEqual(dav?.searchSupportsUploadTime, true, "Dav upload time search should match the provided value") + XCTAssertEqual(dav?.searchSupportsLastActivity, true, "Dav activity search should match the provided value") XCTAssertEqual(dav?.bulkUpload, "1.0", "Dav bulkUpload should match the provided value") + XCTAssertEqual(dav?.absenceSupported, true, "Dav absence support should match the provided value") + XCTAssertEqual(dav?.absenceReplacement, true, "Dav absence replacement should match the provided value") } func testDavInitializationWithInvalidCapabilities() { @@ -39,6 +51,6 @@ class DavTests: XCTestCase { let dav = Dav(capabilities: capabilities) XCTAssertNotNil(dav, "Dav instance should be created with partial input") XCTAssertEqual(dav?.chunking, "1.0", "Dav chunking should match the provided value with partial input") - XCTAssertEqual(dav?.bulkUpload, "", "Dav bulkUpload should default to an empty string with partial input") + XCTAssertNil(dav?.bulkUpload, "Dav bulkUpload should be nil when it is not provided") } } From a4fd7554905cf69a3d1b412466d04e699f04f636 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 14:02:56 +0800 Subject: [PATCH 12/18] Update UserStatus Signed-off-by: Claudio Cambra --- Sources/NextcloudCapabilitiesKit/UserStatus.swift | 8 +++++--- Tests/NextcloudCapabilitiesKitTests/UserStatusTests.swift | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudCapabilitiesKit/UserStatus.swift b/Sources/NextcloudCapabilitiesKit/UserStatus.swift index 582b6b8..d37cdd2 100644 --- a/Sources/NextcloudCapabilitiesKit/UserStatus.swift +++ b/Sources/NextcloudCapabilitiesKit/UserStatus.swift @@ -4,9 +4,10 @@ import Foundation public struct UserStatus: Equatable, Sendable { - let enabled: Bool - let restore: Bool - let supportsEmoji: Bool + public let enabled: Bool + public let restore: Bool + public let supportsEmoji: Bool + public let supportsBusy: Bool init?(capabilities: [String: Any]) { guard let capabilities = capabilities["user_status"] as? [String: Any] else { @@ -17,5 +18,6 @@ public struct UserStatus: Equatable, Sendable { enabled = capabilities["enabled"] as? Bool ?? false restore = capabilities["restore"] as? Bool ?? false supportsEmoji = capabilities["supports_emoji"] as? Bool ?? false + supportsBusy = capabilities["supports_busy"] as? Bool ?? false } } diff --git a/Tests/NextcloudCapabilitiesKitTests/UserStatusTests.swift b/Tests/NextcloudCapabilitiesKitTests/UserStatusTests.swift index 6b7d818..526a8fb 100644 --- a/Tests/NextcloudCapabilitiesKitTests/UserStatusTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/UserStatusTests.swift @@ -11,7 +11,8 @@ class UserStatusTests: XCTestCase { "user_status": [ "enabled": true, "restore": false, - "supports_emoji": true + "supports_emoji": true, + "supports_busy": true ] ] @@ -20,6 +21,7 @@ class UserStatusTests: XCTestCase { XCTAssertTrue(userStatus?.enabled ?? false, "UserStatus enabled should be true") XCTAssertFalse(userStatus?.restore ?? true, "UserStatus restore should be false") XCTAssertTrue(userStatus?.supportsEmoji ?? false, "UserStatus supportsEmoji should be true") + XCTAssertTrue(userStatus?.supportsBusy ?? false, "UserStatus supportsBusy should be true") } func testUserStatusInitializationWithMissingCapabilities() { @@ -43,5 +45,6 @@ class UserStatusTests: XCTestCase { XCTAssertFalse(userStatus?.enabled ?? true, "UserStatus enabled should be false with partial input") XCTAssertFalse(userStatus?.restore ?? true, "UserStatus restore should default to false with partial input") XCTAssertFalse(userStatus?.supportsEmoji ?? true, "UserStatus supportsEmoji should default to false with partial input") + XCTAssertFalse(userStatus?.supportsBusy ?? true, "UserStatus supportsBusy should default to false with partial input") } } From ec78c3ab85a755c88f901aa5730dc572c89f158e Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 14:04:59 +0800 Subject: [PATCH 13/18] Add WeatherStatus Signed-off-by: Claudio Cambra --- .../NextcloudCapabilitiesKit/Capabilities.swift | 2 ++ .../WeatherStatus.swift | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 Sources/NextcloudCapabilitiesKit/WeatherStatus.swift diff --git a/Sources/NextcloudCapabilitiesKit/Capabilities.swift b/Sources/NextcloudCapabilitiesKit/Capabilities.swift index 7250649..881213e 100644 --- a/Sources/NextcloudCapabilitiesKit/Capabilities.swift +++ b/Sources/NextcloudCapabilitiesKit/Capabilities.swift @@ -20,6 +20,7 @@ public struct Capabilities: Equatable, Sendable { public let userStatus: UserStatus? public let filesSharing: FilesSharing? public let notifyPush: NotifyPush? + public let weatherStatus: WeatherStatus? public init?(data: Data) { guard let anyJson = try? JSONSerialization.jsonObject(with: data, options: []) else { @@ -65,5 +66,6 @@ public struct Capabilities: Equatable, Sendable { userStatus = UserStatus(capabilities: capabilities) filesSharing = FilesSharing(capabilities: capabilities) notifyPush = NotifyPush(capabilities: capabilities) + weatherStatus = WeatherStatus(capabilities: capabilities) } } diff --git a/Sources/NextcloudCapabilitiesKit/WeatherStatus.swift b/Sources/NextcloudCapabilitiesKit/WeatherStatus.swift new file mode 100644 index 0000000..677eeb6 --- /dev/null +++ b/Sources/NextcloudCapabilitiesKit/WeatherStatus.swift @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +import Foundation + +public struct WeatherStatus: Equatable, Sendable { + public let enabled: Bool + + init?(capabilities: [String: Any]) { + guard let capabilities = capabilities["weather_status"] as? [String: Any] else { + debugPrint("No weather status data in received capabilities.") + return nil + } + + enabled = capabilities["enabled"] as? Bool ?? false + } +} From ed848f5c97a1d3704b10f6a180284c6ca447bec5 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 14:05:38 +0800 Subject: [PATCH 14/18] Add ProvisioningAPI Signed-off-by: Claudio Cambra --- .../Capabilities.swift | 2 ++ .../ProvisioningAPI.swift | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 Sources/NextcloudCapabilitiesKit/ProvisioningAPI.swift diff --git a/Sources/NextcloudCapabilitiesKit/Capabilities.swift b/Sources/NextcloudCapabilitiesKit/Capabilities.swift index 881213e..bfa0059 100644 --- a/Sources/NextcloudCapabilitiesKit/Capabilities.swift +++ b/Sources/NextcloudCapabilitiesKit/Capabilities.swift @@ -21,6 +21,7 @@ public struct Capabilities: Equatable, Sendable { public let filesSharing: FilesSharing? public let notifyPush: NotifyPush? public let weatherStatus: WeatherStatus? + public let provisioningAPI: ProvisioningAPI? public init?(data: Data) { guard let anyJson = try? JSONSerialization.jsonObject(with: data, options: []) else { @@ -67,5 +68,6 @@ public struct Capabilities: Equatable, Sendable { filesSharing = FilesSharing(capabilities: capabilities) notifyPush = NotifyPush(capabilities: capabilities) weatherStatus = WeatherStatus(capabilities: capabilities) + provisioningAPI = ProvisioningAPI(capabilities: capabilities) } } diff --git a/Sources/NextcloudCapabilitiesKit/ProvisioningAPI.swift b/Sources/NextcloudCapabilitiesKit/ProvisioningAPI.swift new file mode 100644 index 0000000..e38baac --- /dev/null +++ b/Sources/NextcloudCapabilitiesKit/ProvisioningAPI.swift @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +import Foundation + +public struct ProvisioningAPI: Equatable, Sendable { + public let version: String + public let accountPropertyScopesVersion: Int + public let accountPropertyScopesFederatedEnabled: Bool + public let accountPropertyScopesPublishedEnabled: Bool + + init?(capabilities: [String: Any]) { + guard let capabilities = capabilities["provisioning_api"] as? [String: Any] else { + debugPrint("No provisioning API data in received capabilities.") + return nil + } + + version = capabilities["version"] as? String ?? "" + accountPropertyScopesVersion = capabilities["AccountPropertyScopesVersion"] as? Int ?? 0 + accountPropertyScopesFederatedEnabled = capabilities["AccountPropertyScopesFederatedEnabled"] as? Bool ?? false + accountPropertyScopesPublishedEnabled = capabilities["AccountPropertyScopesPublishedEnabled"] as? Bool ?? false + } +} From 9fcd077badbb616adb54ab136cdecf409bce85a5 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 14:05:54 +0800 Subject: [PATCH 15/18] Add SystemTags Signed-off-by: Claudio Cambra --- .../NextcloudCapabilitiesKit/Capabilities.swift | 2 ++ .../NextcloudCapabilitiesKit/SystemTags.swift | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 Sources/NextcloudCapabilitiesKit/SystemTags.swift diff --git a/Sources/NextcloudCapabilitiesKit/Capabilities.swift b/Sources/NextcloudCapabilitiesKit/Capabilities.swift index bfa0059..c56cecb 100644 --- a/Sources/NextcloudCapabilitiesKit/Capabilities.swift +++ b/Sources/NextcloudCapabilitiesKit/Capabilities.swift @@ -20,6 +20,7 @@ public struct Capabilities: Equatable, Sendable { public let userStatus: UserStatus? public let filesSharing: FilesSharing? public let notifyPush: NotifyPush? + public let systemTags: SystemTags? public let weatherStatus: WeatherStatus? public let provisioningAPI: ProvisioningAPI? @@ -67,6 +68,7 @@ public struct Capabilities: Equatable, Sendable { userStatus = UserStatus(capabilities: capabilities) filesSharing = FilesSharing(capabilities: capabilities) notifyPush = NotifyPush(capabilities: capabilities) + systemTags = SystemTags(capabilities: capabilities) weatherStatus = WeatherStatus(capabilities: capabilities) provisioningAPI = ProvisioningAPI(capabilities: capabilities) } diff --git a/Sources/NextcloudCapabilitiesKit/SystemTags.swift b/Sources/NextcloudCapabilitiesKit/SystemTags.swift new file mode 100644 index 0000000..dbb8f5c --- /dev/null +++ b/Sources/NextcloudCapabilitiesKit/SystemTags.swift @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +import Foundation + +public struct SystemTags: Equatable, Sendable { + public let enabled: Bool + + init?(capabilities: [String: Any]) { + guard let capabilities = capabilities["systemtags"] as? [String: Any] else { + debugPrint("No system tags data in received capabilities.") + return nil + } + + enabled = capabilities["enabled"] as? Bool ?? false + } +} From 9e0414b7eb27686d20e6650ea40d4fb99061f373 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 14:06:14 +0800 Subject: [PATCH 16/18] Update Core Signed-off-by: Claudio Cambra --- Sources/NextcloudCapabilitiesKit/Core.swift | 14 +++++++---- .../NextcloudCapabilitiesKit/CoreUser.swift | 23 +++++++++++++++++++ .../CoreTests.swift | 17 +++++++++++++- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 Sources/NextcloudCapabilitiesKit/CoreUser.swift diff --git a/Sources/NextcloudCapabilitiesKit/Core.swift b/Sources/NextcloudCapabilitiesKit/Core.swift index e64d946..f912bc5 100644 --- a/Sources/NextcloudCapabilitiesKit/Core.swift +++ b/Sources/NextcloudCapabilitiesKit/Core.swift @@ -4,10 +4,13 @@ import Foundation public struct Core: Equatable, Sendable { - let pollInterval: Int - let webdavRoot: String - let referenceApi: Bool - let referenceRegex: String + public let pollInterval: Int + public let webdavRoot: String + public let referenceApi: Bool + public let referenceRegex: String + public let modRewriteWorking: Bool + public let user: CoreUser? + public let canCreateAppToken: Bool? init?(capabilities: [String: Any]) { guard let capabilities = capabilities["core"] as? [String: Any] else { @@ -19,5 +22,8 @@ public struct Core: Equatable, Sendable { webdavRoot = capabilities["webdav-root"] as? String ?? "remote.php/webdav" referenceApi = capabilities["reference-api"] as? Bool ?? false referenceRegex = capabilities["reference-regex"] as? String ?? "" + modRewriteWorking = capabilities["mod-rewrite-working"] as? Bool ?? false + user = CoreUser(capabilities: capabilities["user"] as? [String: Any] ?? [:]) + canCreateAppToken = capabilities["can-create-app-token"] as? Bool } } diff --git a/Sources/NextcloudCapabilitiesKit/CoreUser.swift b/Sources/NextcloudCapabilitiesKit/CoreUser.swift new file mode 100644 index 0000000..c8ad6c0 --- /dev/null +++ b/Sources/NextcloudCapabilitiesKit/CoreUser.swift @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +import Foundation + +public struct CoreUser: Equatable, Sendable { + public let language: String + public let locale: String + public let timeZone: String + + init?(capabilities: [String: Any]) { + guard let language = capabilities["language"] as? String, + let locale = capabilities["locale"] as? String, + let timeZone = capabilities["timezone"] as? String + else { + return nil + } + + self.language = language + self.locale = locale + self.timeZone = timeZone + } +} diff --git a/Tests/NextcloudCapabilitiesKitTests/CoreTests.swift b/Tests/NextcloudCapabilitiesKitTests/CoreTests.swift index c35cfb9..54f43cc 100644 --- a/Tests/NextcloudCapabilitiesKitTests/CoreTests.swift +++ b/Tests/NextcloudCapabilitiesKitTests/CoreTests.swift @@ -12,7 +12,14 @@ class CoreTests: XCTestCase { "pollinterval": 30, "webdav-root": "custom/webdav", "reference-api": true, - "reference-regex": "^[a-zA-Z0-9]*$" + "reference-regex": "^[a-zA-Z0-9]*$", + "mod-rewrite-working": true, + "user": [ + "language": "en", + "locale": "en_US", + "timezone": "Europe/Berlin" + ], + "can-create-app-token": true ] ] @@ -22,6 +29,11 @@ class CoreTests: XCTestCase { XCTAssertEqual(core?.webdavRoot, "custom/webdav", "Core webdavRoot should match the provided value") XCTAssertTrue(core?.referenceApi ?? false, "Core referenceApi should match the provided value") XCTAssertEqual(core?.referenceRegex, "^[a-zA-Z0-9]*$", "Core referenceRegex should match the provided value") + XCTAssertTrue(core?.modRewriteWorking ?? false, "Core modRewriteWorking should match the provided value") + XCTAssertEqual(core?.user?.language, "en", "Core user language should match the provided value") + XCTAssertEqual(core?.user?.locale, "en_US", "Core user locale should match the provided value") + XCTAssertEqual(core?.user?.timeZone, "Europe/Berlin", "Core user timezone should match the provided value") + XCTAssertEqual(core?.canCreateAppToken, true, "Core app token capability should match the provided value") } func testCoreInitializationWithInvalidCapabilities() { @@ -46,5 +58,8 @@ class CoreTests: XCTestCase { XCTAssertEqual(core?.webdavRoot, "remote.php/webdav", "Core webdavRoot should default to 'remote.php/webdav' with partial input") XCTAssertFalse(core?.referenceApi ?? true, "Core referenceApi should default to false with partial input") XCTAssertEqual(core?.referenceRegex, "", "Core referenceRegex should default to an empty string with partial input") + XCTAssertFalse(core?.modRewriteWorking ?? true, "Core modRewriteWorking should default to false with partial input") + XCTAssertNil(core?.user, "Core user should be nil when it is not provided") + XCTAssertNil(core?.canCreateAppToken, "Core app token capability should be nil when it is not provided") } } From f1f4cc315af9ed66faf0d6c5766c7b659bf7af32 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 14:06:35 +0800 Subject: [PATCH 17/18] Add Sharing Signed-off-by: Claudio Cambra --- .../Capabilities.swift | 2 ++ .../NextcloudCapabilitiesKit/Sharing.swift | 21 +++++++++++++++++ .../Sharing/SharingPermissionPreset.swift | 23 +++++++++++++++++++ .../Sharing/SharingSourceType.swift | 16 +++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 Sources/NextcloudCapabilitiesKit/Sharing.swift create mode 100644 Sources/NextcloudCapabilitiesKit/Sharing/SharingPermissionPreset.swift create mode 100644 Sources/NextcloudCapabilitiesKit/Sharing/SharingSourceType.swift diff --git a/Sources/NextcloudCapabilitiesKit/Capabilities.swift b/Sources/NextcloudCapabilitiesKit/Capabilities.swift index c56cecb..e411087 100644 --- a/Sources/NextcloudCapabilitiesKit/Capabilities.swift +++ b/Sources/NextcloudCapabilitiesKit/Capabilities.swift @@ -20,6 +20,7 @@ public struct Capabilities: Equatable, Sendable { public let userStatus: UserStatus? public let filesSharing: FilesSharing? public let notifyPush: NotifyPush? + public let sharing: Sharing? public let systemTags: SystemTags? public let weatherStatus: WeatherStatus? public let provisioningAPI: ProvisioningAPI? @@ -68,6 +69,7 @@ public struct Capabilities: Equatable, Sendable { userStatus = UserStatus(capabilities: capabilities) filesSharing = FilesSharing(capabilities: capabilities) notifyPush = NotifyPush(capabilities: capabilities) + sharing = Sharing(capabilities: capabilities) systemTags = SystemTags(capabilities: capabilities) weatherStatus = WeatherStatus(capabilities: capabilities) provisioningAPI = ProvisioningAPI(capabilities: capabilities) diff --git a/Sources/NextcloudCapabilitiesKit/Sharing.swift b/Sources/NextcloudCapabilitiesKit/Sharing.swift new file mode 100644 index 0000000..53eecd5 --- /dev/null +++ b/Sources/NextcloudCapabilitiesKit/Sharing.swift @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +import Foundation + +public struct Sharing: Equatable, Sendable { + public let apiVersions: [String] + public let sourceTypes: [SharingSourceType] + public let permissionPresets: [SharingPermissionPreset] + + init?(capabilities: [String: Any]) { + guard let capabilities = capabilities["sharing"] as? [String: Any] else { + debugPrint("No sharing data in received capabilities.") + return nil + } + + apiVersions = capabilities["api_versions"] as? [String] ?? [] + sourceTypes = (capabilities["source_types"] as? [[String: Any]] ?? []).compactMap(SharingSourceType.init) + permissionPresets = (capabilities["permission_presets"] as? [[String: Any]] ?? []).compactMap(SharingPermissionPreset.init) + } +} diff --git a/Sources/NextcloudCapabilitiesKit/Sharing/SharingPermissionPreset.swift b/Sources/NextcloudCapabilitiesKit/Sharing/SharingPermissionPreset.swift new file mode 100644 index 0000000..90e4d2d --- /dev/null +++ b/Sources/NextcloudCapabilitiesKit/Sharing/SharingPermissionPreset.swift @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +import Foundation + +public struct SharingPermissionPreset: Equatable, Sendable { + public let className: String + public let displayName: String + public let hint: String + + init?(capabilities: [String: Any]) { + guard let className = capabilities["class"] as? String, + let displayName = capabilities["display_name"] as? String, + let hint = capabilities["hint"] as? String + else { + return nil + } + + self.className = className + self.displayName = displayName + self.hint = hint + } +} diff --git a/Sources/NextcloudCapabilitiesKit/Sharing/SharingSourceType.swift b/Sources/NextcloudCapabilitiesKit/Sharing/SharingSourceType.swift new file mode 100644 index 0000000..1e4d002 --- /dev/null +++ b/Sources/NextcloudCapabilitiesKit/Sharing/SharingSourceType.swift @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +import Foundation + +public struct SharingSourceType: Equatable, Sendable { + public let className: String + + init?(capabilities: [String: Any]) { + guard let className = capabilities["class"] as? String else { + return nil + } + + self.className = className + } +} From 26f160125ded5dd455debbb9eb9b2446e95c19ed Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 19 Aug 2026 14:06:39 +0800 Subject: [PATCH 18/18] Add holistic test Signed-off-by: Claudio Cambra --- .../CurrentServerCapabilitiesTests.swift | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 Tests/NextcloudCapabilitiesKitTests/CurrentServerCapabilitiesTests.swift diff --git a/Tests/NextcloudCapabilitiesKitTests/CurrentServerCapabilitiesTests.swift b/Tests/NextcloudCapabilitiesKitTests/CurrentServerCapabilitiesTests.swift new file mode 100644 index 0000000..bdfe54d --- /dev/null +++ b/Tests/NextcloudCapabilitiesKitTests/CurrentServerCapabilitiesTests.swift @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +@testable import NextcloudCapabilitiesKit +import XCTest + +final class CurrentServerCapabilitiesTests: XCTestCase { + func testCurrentServerCapabilities() { + let data = """ + { + "ocs": { + "data": { + "version": { + "major": 33, + "minor": 0, + "micro": 0 + }, + "capabilities": { + "core": { + "pollinterval": 60, + "webdav-root": "remote.php/webdav", + "reference-api": true, + "reference-regex": "regex", + "mod-rewrite-working": true, + "user": { + "language": "en", + "locale": "en_US", + "timezone": "Europe/Berlin" + }, + "can-create-app-token": true + }, + "dav": { + "chunking": "1.0", + "public_shares_chunking": true, + "search_supports_creation_time": true, + "search_supports_upload_time": true, + "search_supports_last_activity": true, + "bulkupload": "1.0", + "absence-supported": true, + "absence-replacement": true + }, + "files": { + "bigfilechunking": true, + "blacklisted_files": ["legacy"], + "forbidden_filenames": ["forbidden"], + "forbidden_filename_basenames": [".DS_Store"], + "forbidden_filename_characters": ["/"], + "forbidden_filename_extensions": [".tmp"], + "chunked_upload": { + "max_size": 1000, + "max_parallel_count": 3 + }, + "file_conversions": [ + { + "from": "application/vnd.oasis.opendocument.text", + "to": "application/pdf", + "extension": "pdf", + "displayName": "PDF" + } + ], + "delete_from_trash": true, + "windows_compatible_filenames": true + }, + "files_sharing": { + "api_enabled": true, + "public": { + "enabled": true, + "multiple_links": true, + "upload_files_drop": true, + "custom_tokens": true + }, + "user": { + "send_mail": false + }, + "resharing": true, + "group_sharing": true, + "group": { + "enabled": true + }, + "default_permissions": 31, + "exclude_reshare_from_edit": true, + "federation": { + "outgoing": true, + "incoming": true, + "expire_date": { + "enabled": true + }, + "expire_date_supported": { + "enabled": true + } + }, + "sharee": { + "minSearchStringLength": 3, + "query_lookup_default": true, + "always_show_unique": true + }, + "sharebymail": { + "enabled": true, + "send_password_by_mail": true, + "upload_files_drop": { + "enabled": true + }, + "password": { + "enabled": true, + "enforced": false + }, + "expire_date": { + "enabled": true, + "enforced": true + } + } + }, + "sharing": { + "api_versions": ["v1"], + "source_types": [ + { + "class": "OCA\\\\Files\\\\Sharing\\\\FileSource" + } + ], + "permission_presets": [ + { + "class": "OCA\\\\Files\\\\Sharing\\\\ReadOnlyPreset", + "display_name": "Read only", + "hint": "Can view but not edit" + } + ] + }, + "systemtags": { + "enabled": true + }, + "weather_status": { + "enabled": true + }, + "provisioning_api": { + "version": "1.0.0", + "AccountPropertyScopesVersion": 2, + "AccountPropertyScopesFederatedEnabled": true, + "AccountPropertyScopesPublishedEnabled": true + }, + "user_status": { + "enabled": true, + "restore": true, + "supports_emoji": true, + "supports_busy": true + } + } + } + } + } + """.data(using: .utf8)! + + let capabilities = Capabilities(data: data) + + XCTAssertEqual(capabilities?.core?.canCreateAppToken, true) + XCTAssertEqual(capabilities?.dav?.absenceReplacement, true) + XCTAssertEqual(capabilities?.files?.forbiddenFilenameExtensions, [".tmp"]) + XCTAssertEqual(capabilities?.files?.fileConversions.first?.displayName, "PDF") + XCTAssertEqual(capabilities?.filesSharing?.publicLink?.multipleAllowed, true) + XCTAssertEqual(capabilities?.filesSharing?.email?.expireDateEnforced, true) + XCTAssertEqual(capabilities?.sharing?.apiVersions, ["v1"]) + XCTAssertEqual(capabilities?.sharing?.sourceTypes.first?.className, "OCA\\Files\\Sharing\\FileSource") + XCTAssertEqual(capabilities?.sharing?.permissionPresets.first?.displayName, "Read only") + XCTAssertEqual(capabilities?.systemTags?.enabled, true) + XCTAssertEqual(capabilities?.weatherStatus?.enabled, true) + XCTAssertEqual(capabilities?.provisioningAPI?.accountPropertyScopesVersion, 2) + XCTAssertEqual(capabilities?.userStatus?.supportsBusy, true) + } +}