From b261899a00ecd9e575e27b97e77e2eb4b4901a0b Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 10:40:09 +0200 Subject: [PATCH 01/16] new extension for getCapabilities Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKInterceptor.swift | 6 +-- Sources/NextcloudKit/NextcloudKit+API.swift | 39 -------------- .../NextcloudKit+Capabilities.swift | 51 +++++++++++++++++++ 3 files changed, 54 insertions(+), 42 deletions(-) create mode 100644 Sources/NextcloudKit/NextcloudKit+Capabilities.swift diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index 232a0945..af3d3289 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -31,19 +31,19 @@ final class NKInterceptor: RequestInterceptor, Sendable { if let array = groupDefaults.array(forKey: nkCommonInstance.groupDefaultsUnauthorized) as? [String], array.contains(account) { - nkLog(tag: "AUTH", message: "Unauthorized for account: \(account)") + nkLog(tag: "AUTH", emoji: .error, message: "Unauthorized for account: \(account)") let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 401)) return completion(.failure(error)) } else if let array = groupDefaults.array(forKey: nkCommonInstance.groupDefaultsUnavailable) as? [String], array.contains(account) { - nkLog(tag: "SERVICE", message: "Unavailable for account: \(account)") + nkLog(tag: "SERVICE", emoji: .error, message: "Unavailable for account: \(account)") let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 503)) return completion(.failure(error)) } else if let array = groupDefaults.array(forKey: nkCommonInstance.groupDefaultsToS) as? [String], array.contains(account) { - nkLog(tag: "TOS", message: "Terms of service error for account: \(account)") + nkLog(tag: "TOS", emoji: .error, message: "Terms of service error for account: \(account)") let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 403)) return completion(.failure(error)) } diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index d0864831..b168959b 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -610,45 +610,6 @@ public extension NextcloudKit { } // MARK: - - func getCapabilities(account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { - let endpoint = "ocs/v1.php/cloud/capabilities" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, .urlError) } - } - - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { - case .failure(let error): - let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, response, error) } - case .success: - options.queue.async { completion(account, response, .success) } - } - } - } - - func getCapabilitiesAsync(account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }) async -> (account: String, responseData: AFDataResponse?, error: NKError) { - await withUnsafeContinuation { continuation in - getCapabilities(account: account, - options: options, - taskHandler: taskHandler) { account, responseData, error in - continuation.resume(returning: (account, responseData, error)) - } - } - } - - // MARK: - - func getRemoteWipeStatus(serverUrl: String, token: String, account: String, diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift new file mode 100644 index 00000000..b2f4a348 --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +#if os(macOS) +import Foundation +import AppKit +#else +import UIKit +#endif +import Alamofire + +public extension NextcloudKit { + + func getCapabilities(account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + let endpoint = "ocs/v1.php/cloud/capabilities" + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, .urlError) } + } + + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, response, error) } + case .success: + options.queue.async { completion(account, response, .success) } + } + } + } + + func getCapabilitiesAsync(account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }) async -> (account: String, responseData: AFDataResponse?, error: NKError) { + await withUnsafeContinuation { continuation in + getCapabilities(account: account, + options: options, + taskHandler: taskHandler) { account, responseData, error in + continuation.resume(returning: (account, responseData, error)) + } + } + } +} From b6eaef6fa9515b3201fc1b22e4e300c53d5b998e Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 11:28:00 +0200 Subject: [PATCH 02/16] added Capabilities Signed-off-by: Marino Faggiana --- .../NextcloudKit+Capabilities.swift | 478 +++++++++++++++++- 1 file changed, 468 insertions(+), 10 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index b2f4a348..47095781 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -10,19 +10,29 @@ import UIKit #endif import Alamofire +// Provides APIs to retrieve and store server capabilities for a Nextcloud account. +// The capabilities endpoint returns server and feature flags, which are parsed, +// cached, and made accessible for feature checks throughout the app. + public extension NextcloudKit { - + + /// Retrieves the capabilities of the Nextcloud server for the given account. + /// - Parameters: + /// - account: The account identifier. + /// - options: Additional request options. + /// - taskHandler: Callback for the underlying URL session task. + /// - completion: Callback returning parsed capabilities or an error. func getCapabilities(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ capabilities: NCCapabilities.Capabilities?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v1.php/cloud/capabilities" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } - + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) @@ -30,22 +40,470 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, response, error) } + options.queue.async { completion(account, nil, response, error) } case .success: - options.queue.async { completion(account, response, .success) } + Task { + do { + let capabilities = try await self.setCapabilitiesAsync(account: account, data: response.data) + options.queue.async { + completion(account, capabilities, response, .success) + } + } catch { + nkLog(debug: "Capabilities decoding failed: \\(error)") + options.queue.async { + completion(account, nil, response, .invalidData) + } + } + } } } } - + + /// Asynchronous wrapper around `getCapabilities`, returning a result tuple. + /// - Parameters: + /// - account: The Nextcloud account identifier. + /// - options: Request options, such as queue, custom headers, etc. + /// - taskHandler: Callback for the underlying `URLSessionTask`. + /// - Returns: A tuple containing account, parsed capabilities, response data, and result error. func getCapabilitiesAsync(account: String, options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }) async -> (account: String, responseData: AFDataResponse?, error: NKError) { + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }) async -> (account: String, + capabilities: NCCapabilities.Capabilities?, + responseData: AFDataResponse?, + error: NKError) { await withUnsafeContinuation { continuation in getCapabilities(account: account, options: options, - taskHandler: taskHandler) { account, responseData, error in - continuation.resume(returning: (account, responseData, error)) + taskHandler: taskHandler) { account, capabilities,responseData, error in + continuation.resume(returning: (account, capabilities, responseData, error)) + } + } + } + + /// Asynchronously decodes and applies server capabilities from JSON data. + /// - Parameters: + /// - account: The Nextcloud account identifier. + /// - data: The raw JSON data returned from the capabilities endpoint. + /// - Returns: A fully populated `NCCapabilities.Capabilities` object. + /// - Throws: An error if decoding fails or data is missing. + func setCapabilitiesAsync(account: String, data: Data? = nil) async throws -> NCCapabilities.Capabilities { + guard let jsonData = data else { + throw NSError(domain: "SetCapabilities", code: 0, userInfo: [NSLocalizedDescriptionKey: "Missing JSON data"]) + } + + struct CapabilityNextcloud: Codable { + struct Ocs: Codable { + let meta: Meta + let data: Data + + struct Meta: Codable { + let status: String? + let message: String? + let statuscode: Int? + } + + struct Data: Codable { + let version: Version + let capabilities: Capabilities + + struct Version: Codable { + let string: String + let major: Int + } + + struct Capabilities: Codable { + let downloadLimit: DownloadLimit? + let filessharing: FilesSharing? + let theming: Theming? + let endtoendencryption: EndToEndEncryption? + let richdocuments: RichDocuments? + let activity: Activity? + let notifications: Notifications? + let files: Files? + let userstatus: UserStatus? + let external: External? + let groupfolders: GroupFolders? + let securityguard: SecurityGuard? + let assistant: Assistant? + let recommendations: Recommendations? + let termsOfService: TermsOfService? + + enum CodingKeys: String, CodingKey { + case downloadLimit = "downloadlimit" + case filessharing = "files_sharing" + case theming + case endtoendencryption = "end-to-end-encryption" + case richdocuments, activity, notifications, files + case userstatus = "user_status" + case external, groupfolders + case securityguard = "security_guard" + case assistant + case recommendations + case termsOfService = "terms_of_service" + } + + struct DownloadLimit: Codable { + let enabled: Bool? + let defaultLimit: Int? + } + + struct FilesSharing: Codable { + let apienabled: Bool? + let groupsharing: Bool? + let resharing: Bool? + let defaultpermissions: Int? + let ncpublic: Public? + + enum CodingKeys: String, CodingKey { + case apienabled = "api_enabled" + case groupsharing = "group_sharing" + case resharing + case defaultpermissions = "default_permissions" + case ncpublic = "public" + } + + struct Public: Codable { + let enabled: Bool + let upload: Bool? + let password: Password? + let sendmail: Bool? + let uploadfilesdrop: Bool? + let multiplelinks: Bool? + let expiredate: ExpireDate? + let expiredateinternal: ExpireDate? + let expiredateremote: ExpireDate? + + enum CodingKeys: String, CodingKey { + case upload, enabled, password + case sendmail = "send_mail" + case uploadfilesdrop = "upload_files_drop" + case multiplelinks = "multiple_links" + case expiredate = "expire_date" + case expiredateinternal = "expire_date_internal" + case expiredateremote = "expire_date_remote" + } + + struct Password: Codable { + let enforced: Bool? + let askForOptionalPassword: Bool? + } + + struct ExpireDate: Codable { + let enforced: Bool? + let days: Int? + } + } + } + + struct Theming: Codable { + let color: String? + let colorelement: String? + let colortext: String? + let colorelementbright: String? + let backgrounddefault: Bool? + let backgroundplain: Bool? + let colorelementdark: String? + let name: String? + let slogan: String? + let url: String? + let logo: String? + let background: String? + let logoheader: String? + let favicon: String? + + enum CodingKeys: String, CodingKey { + case color + case colorelement = "color-element" + case colortext = "color-text" + case colorelementbright = "color-element-bright" + case backgrounddefault = "background-default" + case backgroundplain = "background-plain" + case colorelementdark = "color-element-dark" + case name, slogan, url, logo, background, logoheader, favicon + } + } + + struct EndToEndEncryption: Codable { + let enabled: Bool? + let apiversion: String? + let keysexist: Bool? + + enum CodingKeys: String, CodingKey { + case enabled + case apiversion = "api-version" + case keysexist = "keys-exist" + } + } + + struct RichDocuments: Codable { + let mimetypes: [String]? + let directediting: Bool? + + enum CodingKeys: String, CodingKey { + case mimetypes + case directediting = "direct_editing" + } + } + + struct Activity: Codable { + let apiv2: [String]? + } + + struct Notifications: Codable { + let ocsendpoints: [String]? + + enum CodingKeys: String, CodingKey { + case ocsendpoints = "ocs-endpoints" + } + } + + struct TermsOfService: Codable { + let enabled: Bool? + let termuuid: String? + + enum CodingKeys: String, CodingKey { + case enabled + case termuuid = "term_uuid" + } + } + + struct Files: Codable { + let undelete: Bool? + let locking: String? + let comments: Bool? + let versioning: Bool? + let directEditing: DirectEditing? + let bigfilechunking: Bool? + let versiondeletion: Bool? + let versionlabeling: Bool? + let forbiddenFileNames: [String]? + let forbiddenFileNameBasenames: [String]? + let forbiddenFileNameCharacters: [String]? + let forbiddenFileNameExtensions: [String]? + + enum CodingKeys: String, CodingKey { + case undelete, locking, comments, versioning, directEditing, bigfilechunking + case versiondeletion = "version_deletion" + case versionlabeling = "version_labeling" + case forbiddenFileNames = "forbidden_filenames" + case forbiddenFileNameBasenames = "forbidden_filename_basenames" + case forbiddenFileNameCharacters = "forbidden_filename_characters" + case forbiddenFileNameExtensions = "forbidden_filename_extensions" + } + + struct DirectEditing: Codable { + let url: String? + let etag: String? + let supportsFileId: Bool? + } + } + + struct UserStatus: Codable { + let enabled: Bool? + let restore: Bool? + let supportsemoji: Bool? + + enum CodingKeys: String, CodingKey { + case enabled, restore + case supportsemoji = "supports_emoji" + } + } + + struct External: Codable { + let v1: [String]? + } + + struct GroupFolders: Codable { + let hasGroupFolders: Bool? + } + + struct SecurityGuard: Codable { + let diagnostics: Bool? + } + + struct Assistant: Codable { + let enabled: Bool? + let version: String? + } + + struct Recommendations: Codable { + let enabled: Bool? + } + } + } } + + let ocs: Ocs } + + do { + // Decode the full JSON structure + let decoded = try JSONDecoder().decode(CapabilityNextcloud.self, from: jsonData) + let data = decoded.ocs.data + let json = data.capabilities + + // Initialize capabilities + let capabilities = NCCapabilities.Capabilities() + + // Version info + capabilities.capabilityServerVersion = data.version.string + capabilities.capabilityServerVersionMajor = data.version.major + + // Update NextcloudKit session if needed + if capabilities.capabilityServerVersionMajor > 0 { + NextcloudKit.shared.updateSession(account: account, nextcloudVersion: capabilities.capabilityServerVersionMajor) + } + + // Populate capabilities from decoded JSON + capabilities.capabilityFileSharingApiEnabled = json.filessharing?.apienabled ?? false + capabilities.capabilityFileSharingDefaultPermission = json.filessharing?.defaultpermissions ?? 0 + capabilities.capabilityFileSharingPubPasswdEnforced = json.filessharing?.ncpublic?.password?.enforced ?? false + capabilities.capabilityFileSharingPubExpireDateEnforced = json.filessharing?.ncpublic?.expiredate?.enforced ?? false + capabilities.capabilityFileSharingPubExpireDateDays = json.filessharing?.ncpublic?.expiredate?.days ?? 0 + capabilities.capabilityFileSharingInternalExpireDateEnforced = json.filessharing?.ncpublic?.expiredateinternal?.enforced ?? false + capabilities.capabilityFileSharingInternalExpireDateDays = json.filessharing?.ncpublic?.expiredateinternal?.days ?? 0 + capabilities.capabilityFileSharingRemoteExpireDateEnforced = json.filessharing?.ncpublic?.expiredateremote?.enforced ?? false + capabilities.capabilityFileSharingRemoteExpireDateDays = json.filessharing?.ncpublic?.expiredateremote?.days ?? 0 + capabilities.capabilityFileSharingDownloadLimit = json.downloadLimit?.enabled ?? false + capabilities.capabilityFileSharingDownloadLimitDefaultLimit = json.downloadLimit?.defaultLimit ?? 1 + + capabilities.capabilityThemingColor = json.theming?.color ?? "" + capabilities.capabilityThemingColorElement = json.theming?.colorelement ?? "" + capabilities.capabilityThemingColorText = json.theming?.colortext ?? "" + capabilities.capabilityThemingName = json.theming?.name ?? "" + capabilities.capabilityThemingSlogan = json.theming?.slogan ?? "" + + capabilities.capabilityE2EEEnabled = json.endtoendencryption?.enabled ?? false + capabilities.capabilityE2EEApiVersion = json.endtoendencryption?.apiversion ?? "" + + capabilities.capabilityRichDocumentsEnabled = json.richdocuments?.directediting ?? false + capabilities.capabilityRichDocumentsMimetypes.removeAll() + capabilities.capabilityRichDocumentsMimetypes = json.richdocuments?.mimetypes ?? [] + + capabilities.capabilityAssistantEnabled = json.assistant?.enabled ?? false + + capabilities.capabilityActivityEnabled = json.activity != nil + capabilities.capabilityActivity = json.activity?.apiv2 ?? [] + + capabilities.capabilityNotification = json.notifications?.ocsendpoints ?? [] + + capabilities.capabilityFilesUndelete = json.files?.undelete ?? false + capabilities.capabilityFilesLockVersion = json.files?.locking ?? "" + capabilities.capabilityFilesComments = json.files?.comments ?? false + capabilities.capabilityFilesBigfilechunking = json.files?.bigfilechunking ?? false + + capabilities.capabilityUserStatusEnabled = json.userstatus?.enabled ?? false + capabilities.capabilityExternalSites = json.external != nil + capabilities.capabilityGroupfoldersEnabled = json.groupfolders?.hasGroupFolders ?? false + + if capabilities.capabilityServerVersionMajor >= 28 { + capabilities.isLivePhotoServerAvailable = true + } + + capabilities.capabilitySecurityGuardDiagnostics = json.securityguard?.diagnostics ?? false + + capabilities.capabilityForbiddenFileNames = json.files?.forbiddenFileNames ?? [] + capabilities.capabilityForbiddenFileNameBasenames = json.files?.forbiddenFileNameBasenames ?? [] + capabilities.capabilityForbiddenFileNameCharacters = json.files?.forbiddenFileNameCharacters ?? [] + capabilities.capabilityForbiddenFileNameExtensions = json.files?.forbiddenFileNameExtensions ?? [] + + capabilities.capabilityRecommendations = json.recommendations?.enabled ?? false + capabilities.capabilityTermsOfService = json.termsOfService?.enabled ?? false + + // Persist capabilities in shared store + await NCCapabilities.shared.appendCapabilities(for: account, capabilities: capabilities) + return capabilities + + } catch { + nkLog(debug: "Could not decode json capabilities: \(error.localizedDescription)") + throw error + } + } +} + +/// A concurrency-safe store for capabilities associated with Nextcloud accounts. +actor CapabilitiesStore { + private var store: [String: NCCapabilities.Capabilities] = [:] + + func get(_ account: String) -> NCCapabilities.Capabilities? { + return store[account] + } + + func set(_ account: String, value: NCCapabilities.Capabilities) { + store[account] = value + } + + func shouldDisableSharesView(for account: String) -> Bool { + guard let capability = store[account] else { + return true + } + return (!capability.capabilityFileSharingApiEnabled && + !capability.capabilityFilesComments && + capability.capabilityActivity.isEmpty) + } +} + +/// Singleton container and public API for accessing and caching capabilities. +final public class NCCapabilities: Sendable { + static let shared = NCCapabilities() + + private let store = CapabilitiesStore() + + public class Capabilities: @unchecked Sendable { + var capabilityServerVersionMajor: Int = 0 + var capabilityServerVersion: String = "" + var capabilityFileSharingApiEnabled: Bool = false + var capabilityFileSharingPubPasswdEnforced: Bool = false + var capabilityFileSharingPubExpireDateEnforced: Bool = false + var capabilityFileSharingPubExpireDateDays: Int = 0 + var capabilityFileSharingInternalExpireDateEnforced: Bool = false + var capabilityFileSharingInternalExpireDateDays: Int = 0 + var capabilityFileSharingRemoteExpireDateEnforced: Bool = false + var capabilityFileSharingRemoteExpireDateDays: Int = 0 + var capabilityFileSharingDefaultPermission: Int = 0 + var capabilityFileSharingDownloadLimit: Bool = false + var capabilityFileSharingDownloadLimitDefaultLimit: Int = 1 + var capabilityThemingColor: String = "" + var capabilityThemingColorElement: String = "" + var capabilityThemingColorText: String = "" + var capabilityThemingName: String = "" + var capabilityThemingSlogan: String = "" + var capabilityE2EEEnabled: Bool = false + var capabilityE2EEApiVersion: String = "" + var capabilityRichDocumentsEnabled: Bool = false + var capabilityRichDocumentsMimetypes: [String] = [] + var capabilityActivity: [String] = [] + var capabilityNotification: [String] = [] + var capabilityFilesUndelete: Bool = false + var capabilityFilesLockVersion: String = "" // NC 24 + var capabilityFilesComments: Bool = false // NC 20 + var capabilityFilesBigfilechunking: Bool = false + var capabilityUserStatusEnabled: Bool = false + var capabilityExternalSites: Bool = false + var capabilityActivityEnabled: Bool = false + var capabilityGroupfoldersEnabled: Bool = false // NC27 + var capabilityAssistantEnabled: Bool = false // NC28 + var isLivePhotoServerAvailable: Bool = false // NC28 + var capabilitySecurityGuardDiagnostics = false + var capabilityForbiddenFileNames: [String] = [] + var capabilityForbiddenFileNameBasenames: [String] = [] + var capabilityForbiddenFileNameCharacters: [String] = [] + var capabilityForbiddenFileNameExtensions: [String] = [] + var capabilityRecommendations: Bool = false + var capabilityTermsOfService: Bool = false + } + + // MARK: - Public API + + public func disableSharesView(for account: String) async -> Bool { + await store.shouldDisableSharesView(for: account) + } + + public func getCapabilities(for account: String) async -> Capabilities? { + await store.get(account) + } + + public func appendCapabilities(for account: String, capabilities: Capabilities) async { + await store.set(account, value: capabilities) } } From 1d0421bb34d72f0f7b02a3cf14821f527a4f985b Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 11:45:10 +0200 Subject: [PATCH 03/16] public Signed-off-by: Marino Faggiana --- .../NextcloudKit+Capabilities.swift | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index 47095781..ee5e4ee8 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -450,47 +450,47 @@ final public class NCCapabilities: Sendable { private let store = CapabilitiesStore() public class Capabilities: @unchecked Sendable { - var capabilityServerVersionMajor: Int = 0 - var capabilityServerVersion: String = "" - var capabilityFileSharingApiEnabled: Bool = false - var capabilityFileSharingPubPasswdEnforced: Bool = false - var capabilityFileSharingPubExpireDateEnforced: Bool = false - var capabilityFileSharingPubExpireDateDays: Int = 0 - var capabilityFileSharingInternalExpireDateEnforced: Bool = false - var capabilityFileSharingInternalExpireDateDays: Int = 0 - var capabilityFileSharingRemoteExpireDateEnforced: Bool = false - var capabilityFileSharingRemoteExpireDateDays: Int = 0 - var capabilityFileSharingDefaultPermission: Int = 0 - var capabilityFileSharingDownloadLimit: Bool = false - var capabilityFileSharingDownloadLimitDefaultLimit: Int = 1 - var capabilityThemingColor: String = "" - var capabilityThemingColorElement: String = "" - var capabilityThemingColorText: String = "" - var capabilityThemingName: String = "" - var capabilityThemingSlogan: String = "" - var capabilityE2EEEnabled: Bool = false - var capabilityE2EEApiVersion: String = "" - var capabilityRichDocumentsEnabled: Bool = false - var capabilityRichDocumentsMimetypes: [String] = [] - var capabilityActivity: [String] = [] - var capabilityNotification: [String] = [] - var capabilityFilesUndelete: Bool = false - var capabilityFilesLockVersion: String = "" // NC 24 - var capabilityFilesComments: Bool = false // NC 20 - var capabilityFilesBigfilechunking: Bool = false - var capabilityUserStatusEnabled: Bool = false - var capabilityExternalSites: Bool = false - var capabilityActivityEnabled: Bool = false - var capabilityGroupfoldersEnabled: Bool = false // NC27 - var capabilityAssistantEnabled: Bool = false // NC28 - var isLivePhotoServerAvailable: Bool = false // NC28 - var capabilitySecurityGuardDiagnostics = false - var capabilityForbiddenFileNames: [String] = [] - var capabilityForbiddenFileNameBasenames: [String] = [] - var capabilityForbiddenFileNameCharacters: [String] = [] - var capabilityForbiddenFileNameExtensions: [String] = [] - var capabilityRecommendations: Bool = false - var capabilityTermsOfService: Bool = false + public var capabilityServerVersionMajor: Int = 0 + public var capabilityServerVersion: String = "" + public var capabilityFileSharingApiEnabled: Bool = false + public var capabilityFileSharingPubPasswdEnforced: Bool = false + public var capabilityFileSharingPubExpireDateEnforced: Bool = false + public var capabilityFileSharingPubExpireDateDays: Int = 0 + public var capabilityFileSharingInternalExpireDateEnforced: Bool = false + public var capabilityFileSharingInternalExpireDateDays: Int = 0 + public var capabilityFileSharingRemoteExpireDateEnforced: Bool = false + public var capabilityFileSharingRemoteExpireDateDays: Int = 0 + public var capabilityFileSharingDefaultPermission: Int = 0 + public var capabilityFileSharingDownloadLimit: Bool = false + public var capabilityFileSharingDownloadLimitDefaultLimit: Int = 1 + public var capabilityThemingColor: String = "" + public var capabilityThemingColorElement: String = "" + public var capabilityThemingColorText: String = "" + public var capabilityThemingName: String = "" + public var capabilityThemingSlogan: String = "" + public var capabilityE2EEEnabled: Bool = false + public var capabilityE2EEApiVersion: String = "" + public var capabilityRichDocumentsEnabled: Bool = false + public var capabilityRichDocumentsMimetypes: [String] = [] + public var capabilityActivity: [String] = [] + public var capabilityNotification: [String] = [] + public var capabilityFilesUndelete: Bool = false + public var capabilityFilesLockVersion: String = "" // NC 24 + public var capabilityFilesComments: Bool = false // NC 20 + public var capabilityFilesBigfilechunking: Bool = false + public var capabilityUserStatusEnabled: Bool = false + public var capabilityExternalSites: Bool = false + public var capabilityActivityEnabled: Bool = false + public var capabilityGroupfoldersEnabled: Bool = false // NC27 + public var capabilityAssistantEnabled: Bool = false // NC28 + public var isLivePhotoServerAvailable: Bool = false // NC28 + public var capabilitySecurityGuardDiagnostics = false + public var capabilityForbiddenFileNames: [String] = [] + public var capabilityForbiddenFileNameBasenames: [String] = [] + public var capabilityForbiddenFileNameCharacters: [String] = [] + public var capabilityForbiddenFileNameExtensions: [String] = [] + public var capabilityRecommendations: Bool = false + public var capabilityTermsOfService: Bool = false } // MARK: - Public API From f1e65a8c41ced18885d2918d34f867ae9ab8750d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 11:54:08 +0200 Subject: [PATCH 04/16] log Signed-off-by: Marino Faggiana --- .../Extensions/Data+Extension.swift | 36 +++++++++++++++++++ .../NextcloudKit+Capabilities.swift | 6 +++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 Sources/NextcloudKit/Extensions/Data+Extension.swift diff --git a/Sources/NextcloudKit/Extensions/Data+Extension.swift b/Sources/NextcloudKit/Extensions/Data+Extension.swift new file mode 100644 index 00000000..f8131c12 --- /dev/null +++ b/Sources/NextcloudKit/Extensions/Data+Extension.swift @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +extension Data { + func printJson() { + do { + let json = try JSONSerialization.jsonObject(with: self, options: []) + let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) + guard let jsonString = String(data: data, encoding: .utf8) else { + print("Inavlid data") + return + } + print(jsonString) + } catch { + print("Error: \(error.localizedDescription)") + } + } + + func jsonToString() -> String { + do { + let json = try JSONSerialization.jsonObject(with: self, options: []) + let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) + guard let jsonString = String(data: data, encoding: .utf8) else { + print("Inavlid data") + return "" + } + return jsonString + } catch { + print("Error: \(error.localizedDescription)") + } + return "" + } +} diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index ee5e4ee8..c2f08f4c 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -336,6 +336,10 @@ public extension NextcloudKit { let ocs: Ocs } + if NKLogFileManager.shared.logLevel >= .normal { + jsonData.printJson() + } + do { // Decode the full JSON structure let decoded = try JSONDecoder().decode(CapabilityNextcloud.self, from: jsonData) @@ -415,7 +419,7 @@ public extension NextcloudKit { return capabilities } catch { - nkLog(debug: "Could not decode json capabilities: \(error.localizedDescription)") + nkLog(error: "Could not decode json capabilities: \(error.localizedDescription)") throw error } } From 0822e4528d3f7fcb379391c8cab3e7ec50195645 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 12:09:16 +0200 Subject: [PATCH 05/16] public Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Capabilities.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index c2f08f4c..423806ba 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -449,7 +449,7 @@ actor CapabilitiesStore { /// Singleton container and public API for accessing and caching capabilities. final public class NCCapabilities: Sendable { - static let shared = NCCapabilities() + public static let shared = NCCapabilities() private let store = CapabilitiesStore() From f95ff59477363bc4109bf2b3d3a740affe00a72c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 12:22:21 +0200 Subject: [PATCH 06/16] remove nextcloudVersion in session Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 3 --- Sources/NextcloudKit/NextcloudKit+Capabilities.swift | 5 ----- Sources/NextcloudKit/NextcloudKit.swift | 8 +------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index abb72c18..df224cb7 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -12,7 +12,6 @@ public struct NKSession: Sendable { public var password: String public var account: String public var userAgent: String - public var nextcloudVersion: Int public let groupIdentifier: String public let httpMaximumConnectionsPerHost: Int public let httpMaximumConnectionsPerHostInDownload: Int @@ -33,7 +32,6 @@ public struct NKSession: Sendable { password: String, account: String, userAgent: String, - nextcloudVersion: Int, groupIdentifier: String, httpMaximumConnectionsPerHost: Int, httpMaximumConnectionsPerHostInDownload: Int, @@ -44,7 +42,6 @@ public struct NKSession: Sendable { self.password = password self.account = account self.userAgent = userAgent - self.nextcloudVersion = nextcloudVersion self.groupIdentifier = groupIdentifier self.httpMaximumConnectionsPerHost = httpMaximumConnectionsPerHost self.httpMaximumConnectionsPerHostInDownload = httpMaximumConnectionsPerHostInDownload diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index 423806ba..0ac84e65 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -353,11 +353,6 @@ public extension NextcloudKit { capabilities.capabilityServerVersion = data.version.string capabilities.capabilityServerVersionMajor = data.version.major - // Update NextcloudKit session if needed - if capabilities.capabilityServerVersionMajor > 0 { - NextcloudKit.shared.updateSession(account: account, nextcloudVersion: capabilities.capabilityServerVersionMajor) - } - // Populate capabilities from decoded JSON capabilities.capabilityFileSharingApiEnabled = json.filessharing?.apienabled ?? false capabilities.capabilityFileSharingDefaultPermission = json.filessharing?.defaultpermissions ?? 0 diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 3ff2257c..bb85c609 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -83,13 +83,12 @@ open class NextcloudKit { userId: String, password: String, userAgent: String, - nextcloudVersion: Int, httpMaximumConnectionsPerHost: Int = 6, httpMaximumConnectionsPerHostInDownload: Int = 6, httpMaximumConnectionsPerHostInUpload: Int = 6, groupIdentifier: String) { if nkCommonInstance.nksessions.filter({ $0.account == account }).first != nil { - return updateSession(account: account, urlBase: urlBase, userId: userId, password: password, userAgent: userAgent, nextcloudVersion: nextcloudVersion) + return updateSession(account: account, urlBase: urlBase, userId: userId, password: password, userAgent: userAgent) } let nkSession = NKSession( @@ -100,7 +99,6 @@ open class NextcloudKit { password: password, account: account, userAgent: userAgent, - nextcloudVersion: nextcloudVersion, groupIdentifier: groupIdentifier, httpMaximumConnectionsPerHost: httpMaximumConnectionsPerHost, httpMaximumConnectionsPerHostInDownload: httpMaximumConnectionsPerHostInDownload, @@ -116,7 +114,6 @@ open class NextcloudKit { userId: String? = nil, password: String? = nil, userAgent: String? = nil, - nextcloudVersion: Int? = nil, replaceWithAccount: String? = nil) { guard var nkSession = nkCommonInstance.nksessions.filter({ $0.account == account }).first else { return } if let urlBase { @@ -134,9 +131,6 @@ open class NextcloudKit { if let userAgent { nkSession.userAgent = userAgent } - if let nextcloudVersion { - nkSession.nextcloudVersion = nextcloudVersion - } if let replaceWithAccount { nkSession.account = replaceWithAccount } From eaee3da7829111b1e0a95232c784102fcf1ccc8b Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 13:32:49 +0200 Subject: [PATCH 07/16] getCapabilitiesBlocking Signed-off-by: Marino Faggiana --- .../NextcloudKit+Capabilities.swift | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index 0ac84e65..e2dac4ff 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -498,11 +498,28 @@ final public class NCCapabilities: Sendable { await store.shouldDisableSharesView(for: account) } + public func appendCapabilities(for account: String, capabilities: Capabilities) async { + await store.set(account, value: capabilities) + } + public func getCapabilities(for account: String) async -> Capabilities? { await store.get(account) } - public func appendCapabilities(for account: String, capabilities: Capabilities) async { - await store.set(account, value: capabilities) + /// Synchronously retrieves capabilities for the given account. + /// Blocks the current thread until the async actor returns. + /// Use only outside the Swift async context (never from another actor or async function). + public func getCapabilitiesBlocking(for account: String) -> Capabilities? { + let group = DispatchGroup() + var result: Capabilities? + + group.enter() + Task { + result = await store.get(account) + group.leave() + } + + group.wait() + return result } } From 94816766413d2ad96851867364ada57d36290786 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 14:00:54 +0200 Subject: [PATCH 08/16] capabilities Signed-off-by: Marino Faggiana --- .../NextcloudKit/NextcloudKit+Capabilities.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index e2dac4ff..d4abd49f 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -502,6 +502,21 @@ final public class NCCapabilities: Sendable { await store.set(account, value: capabilities) } + /// Synchronously stores capabilities for the given account. + /// Blocks the current thread until the async actor completes. + /// Use only outside of async/actor contexts. + public func appendCapabilitiesBlocking(for account: String, capabilities: Capabilities) { + let group = DispatchGroup() + + group.enter() + Task { + await store.set(account, value: capabilities) + group.leave() + } + + group.wait() + } + public func getCapabilities(for account: String) async -> Capabilities? { await store.get(account) } From b339b311f5048a5616f598d4c43f86eb6696bd67 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 14:18:05 +0200 Subject: [PATCH 09/16] init Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Capabilities.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index d4abd49f..0a0668e5 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -490,6 +490,8 @@ final public class NCCapabilities: Sendable { public var capabilityForbiddenFileNameExtensions: [String] = [] public var capabilityRecommendations: Bool = false public var capabilityTermsOfService: Bool = false + + public init() {} } // MARK: - Public API From 7f6cafc62175a1a621725f4e1909b47448e539bb Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 14:28:12 +0200 Subject: [PATCH 10/16] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Capabilities.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index 0a0668e5..3f25bde7 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -519,14 +519,14 @@ final public class NCCapabilities: Sendable { group.wait() } - public func getCapabilities(for account: String) async -> Capabilities? { - await store.get(account) + public func getCapabilities(for account: String) async -> Capabilities { + await store.get(account) ?? Capabilities() } /// Synchronously retrieves capabilities for the given account. /// Blocks the current thread until the async actor returns. /// Use only outside the Swift async context (never from another actor or async function). - public func getCapabilitiesBlocking(for account: String) -> Capabilities? { + public func getCapabilitiesBlocking(for account: String) -> Capabilities { let group = DispatchGroup() var result: Capabilities? @@ -537,6 +537,6 @@ final public class NCCapabilities: Sendable { } group.wait() - return result + return result ?? Capabilities() } } From 22e462af7e83544cc6771fb4f13e7af5dcdc5b5c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 14:46:48 +0200 Subject: [PATCH 11/16] cod Signed-off-by: Marino Faggiana --- .../NextcloudKit+Capabilities.swift | 190 +++++++++--------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index 3f25bde7..4f69fa6b 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -350,64 +350,64 @@ public extension NextcloudKit { let capabilities = NCCapabilities.Capabilities() // Version info - capabilities.capabilityServerVersion = data.version.string - capabilities.capabilityServerVersionMajor = data.version.major + capabilities.serverVersion = data.version.string + capabilities.serverVersionMajor = data.version.major // Populate capabilities from decoded JSON - capabilities.capabilityFileSharingApiEnabled = json.filessharing?.apienabled ?? false - capabilities.capabilityFileSharingDefaultPermission = json.filessharing?.defaultpermissions ?? 0 - capabilities.capabilityFileSharingPubPasswdEnforced = json.filessharing?.ncpublic?.password?.enforced ?? false - capabilities.capabilityFileSharingPubExpireDateEnforced = json.filessharing?.ncpublic?.expiredate?.enforced ?? false - capabilities.capabilityFileSharingPubExpireDateDays = json.filessharing?.ncpublic?.expiredate?.days ?? 0 - capabilities.capabilityFileSharingInternalExpireDateEnforced = json.filessharing?.ncpublic?.expiredateinternal?.enforced ?? false - capabilities.capabilityFileSharingInternalExpireDateDays = json.filessharing?.ncpublic?.expiredateinternal?.days ?? 0 - capabilities.capabilityFileSharingRemoteExpireDateEnforced = json.filessharing?.ncpublic?.expiredateremote?.enforced ?? false - capabilities.capabilityFileSharingRemoteExpireDateDays = json.filessharing?.ncpublic?.expiredateremote?.days ?? 0 - capabilities.capabilityFileSharingDownloadLimit = json.downloadLimit?.enabled ?? false - capabilities.capabilityFileSharingDownloadLimitDefaultLimit = json.downloadLimit?.defaultLimit ?? 1 - - capabilities.capabilityThemingColor = json.theming?.color ?? "" - capabilities.capabilityThemingColorElement = json.theming?.colorelement ?? "" - capabilities.capabilityThemingColorText = json.theming?.colortext ?? "" - capabilities.capabilityThemingName = json.theming?.name ?? "" - capabilities.capabilityThemingSlogan = json.theming?.slogan ?? "" - - capabilities.capabilityE2EEEnabled = json.endtoendencryption?.enabled ?? false - capabilities.capabilityE2EEApiVersion = json.endtoendencryption?.apiversion ?? "" - - capabilities.capabilityRichDocumentsEnabled = json.richdocuments?.directediting ?? false - capabilities.capabilityRichDocumentsMimetypes.removeAll() - capabilities.capabilityRichDocumentsMimetypes = json.richdocuments?.mimetypes ?? [] - - capabilities.capabilityAssistantEnabled = json.assistant?.enabled ?? false - - capabilities.capabilityActivityEnabled = json.activity != nil - capabilities.capabilityActivity = json.activity?.apiv2 ?? [] - - capabilities.capabilityNotification = json.notifications?.ocsendpoints ?? [] - - capabilities.capabilityFilesUndelete = json.files?.undelete ?? false - capabilities.capabilityFilesLockVersion = json.files?.locking ?? "" - capabilities.capabilityFilesComments = json.files?.comments ?? false - capabilities.capabilityFilesBigfilechunking = json.files?.bigfilechunking ?? false - - capabilities.capabilityUserStatusEnabled = json.userstatus?.enabled ?? false - capabilities.capabilityExternalSites = json.external != nil - capabilities.capabilityGroupfoldersEnabled = json.groupfolders?.hasGroupFolders ?? false - - if capabilities.capabilityServerVersionMajor >= 28 { + capabilities.fileSharingApiEnabled = json.filessharing?.apienabled ?? false + capabilities.fileSharingDefaultPermission = json.filessharing?.defaultpermissions ?? 0 + capabilities.fileSharingPubPasswdEnforced = json.filessharing?.ncpublic?.password?.enforced ?? false + capabilities.fileSharingPubExpireDateEnforced = json.filessharing?.ncpublic?.expiredate?.enforced ?? false + capabilities.fileSharingPubExpireDateDays = json.filessharing?.ncpublic?.expiredate?.days ?? 0 + capabilities.fileSharingInternalExpireDateEnforced = json.filessharing?.ncpublic?.expiredateinternal?.enforced ?? false + capabilities.fileSharingInternalExpireDateDays = json.filessharing?.ncpublic?.expiredateinternal?.days ?? 0 + capabilities.fileSharingRemoteExpireDateEnforced = json.filessharing?.ncpublic?.expiredateremote?.enforced ?? false + capabilities.fileSharingRemoteExpireDateDays = json.filessharing?.ncpublic?.expiredateremote?.days ?? 0 + capabilities.fileSharingDownloadLimit = json.downloadLimit?.enabled ?? false + capabilities.fileSharingDownloadLimitDefaultLimit = json.downloadLimit?.defaultLimit ?? 1 + + capabilities.themingColor = json.theming?.color ?? "" + capabilities.themingColorElement = json.theming?.colorelement ?? "" + capabilities.themingColorText = json.theming?.colortext ?? "" + capabilities.themingName = json.theming?.name ?? "" + capabilities.themingSlogan = json.theming?.slogan ?? "" + + capabilities.e2EEEnabled = json.endtoendencryption?.enabled ?? false + capabilities.e2EEApiVersion = json.endtoendencryption?.apiversion ?? "" + + capabilities.richDocumentsEnabled = json.richdocuments?.directediting ?? false + capabilities.richDocumentsMimetypes.removeAll() + capabilities.richDocumentsMimetypes = json.richdocuments?.mimetypes ?? [] + + capabilities.assistantEnabled = json.assistant?.enabled ?? false + + capabilities.activityEnabled = json.activity != nil + capabilities.activity = json.activity?.apiv2 ?? [] + + capabilities.notification = json.notifications?.ocsendpoints ?? [] + + capabilities.filesUndelete = json.files?.undelete ?? false + capabilities.filesLockVersion = json.files?.locking ?? "" + capabilities.filesComments = json.files?.comments ?? false + capabilities.filesBigfilechunking = json.files?.bigfilechunking ?? false + + capabilities.userStatusEnabled = json.userstatus?.enabled ?? false + capabilities.externalSites = json.external != nil + capabilities.groupfoldersEnabled = json.groupfolders?.hasGroupFolders ?? false + + if capabilities.serverVersionMajor >= 28 { capabilities.isLivePhotoServerAvailable = true } - capabilities.capabilitySecurityGuardDiagnostics = json.securityguard?.diagnostics ?? false + capabilities.securityGuardDiagnostics = json.securityguard?.diagnostics ?? false - capabilities.capabilityForbiddenFileNames = json.files?.forbiddenFileNames ?? [] - capabilities.capabilityForbiddenFileNameBasenames = json.files?.forbiddenFileNameBasenames ?? [] - capabilities.capabilityForbiddenFileNameCharacters = json.files?.forbiddenFileNameCharacters ?? [] - capabilities.capabilityForbiddenFileNameExtensions = json.files?.forbiddenFileNameExtensions ?? [] + capabilities.forbiddenFileNames = json.files?.forbiddenFileNames ?? [] + capabilities.forbiddenFileNameBasenames = json.files?.forbiddenFileNameBasenames ?? [] + capabilities.forbiddenFileNameCharacters = json.files?.forbiddenFileNameCharacters ?? [] + capabilities.forbiddenFileNameExtensions = json.files?.forbiddenFileNameExtensions ?? [] - capabilities.capabilityRecommendations = json.recommendations?.enabled ?? false - capabilities.capabilityTermsOfService = json.termsOfService?.enabled ?? false + capabilities.recommendations = json.recommendations?.enabled ?? false + capabilities.termsOfService = json.termsOfService?.enabled ?? false // Persist capabilities in shared store await NCCapabilities.shared.appendCapabilities(for: account, capabilities: capabilities) @@ -436,9 +436,9 @@ actor CapabilitiesStore { guard let capability = store[account] else { return true } - return (!capability.capabilityFileSharingApiEnabled && - !capability.capabilityFilesComments && - capability.capabilityActivity.isEmpty) + return (!capability.fileSharingApiEnabled && + !capability.filesComments && + capability.activity.isEmpty) } } @@ -449,47 +449,47 @@ final public class NCCapabilities: Sendable { private let store = CapabilitiesStore() public class Capabilities: @unchecked Sendable { - public var capabilityServerVersionMajor: Int = 0 - public var capabilityServerVersion: String = "" - public var capabilityFileSharingApiEnabled: Bool = false - public var capabilityFileSharingPubPasswdEnforced: Bool = false - public var capabilityFileSharingPubExpireDateEnforced: Bool = false - public var capabilityFileSharingPubExpireDateDays: Int = 0 - public var capabilityFileSharingInternalExpireDateEnforced: Bool = false - public var capabilityFileSharingInternalExpireDateDays: Int = 0 - public var capabilityFileSharingRemoteExpireDateEnforced: Bool = false - public var capabilityFileSharingRemoteExpireDateDays: Int = 0 - public var capabilityFileSharingDefaultPermission: Int = 0 - public var capabilityFileSharingDownloadLimit: Bool = false - public var capabilityFileSharingDownloadLimitDefaultLimit: Int = 1 - public var capabilityThemingColor: String = "" - public var capabilityThemingColorElement: String = "" - public var capabilityThemingColorText: String = "" - public var capabilityThemingName: String = "" - public var capabilityThemingSlogan: String = "" - public var capabilityE2EEEnabled: Bool = false - public var capabilityE2EEApiVersion: String = "" - public var capabilityRichDocumentsEnabled: Bool = false - public var capabilityRichDocumentsMimetypes: [String] = [] - public var capabilityActivity: [String] = [] - public var capabilityNotification: [String] = [] - public var capabilityFilesUndelete: Bool = false - public var capabilityFilesLockVersion: String = "" // NC 24 - public var capabilityFilesComments: Bool = false // NC 20 - public var capabilityFilesBigfilechunking: Bool = false - public var capabilityUserStatusEnabled: Bool = false - public var capabilityExternalSites: Bool = false - public var capabilityActivityEnabled: Bool = false - public var capabilityGroupfoldersEnabled: Bool = false // NC27 - public var capabilityAssistantEnabled: Bool = false // NC28 - public var isLivePhotoServerAvailable: Bool = false // NC28 - public var capabilitySecurityGuardDiagnostics = false - public var capabilityForbiddenFileNames: [String] = [] - public var capabilityForbiddenFileNameBasenames: [String] = [] - public var capabilityForbiddenFileNameCharacters: [String] = [] - public var capabilityForbiddenFileNameExtensions: [String] = [] - public var capabilityRecommendations: Bool = false - public var capabilityTermsOfService: Bool = false + public var serverVersionMajor: Int = 0 + public var serverVersion: String = "" + public var fileSharingApiEnabled: Bool = false + public var fileSharingPubPasswdEnforced: Bool = false + public var fileSharingPubExpireDateEnforced: Bool = false + public var fileSharingPubExpireDateDays: Int = 0 + public var fileSharingInternalExpireDateEnforced: Bool = false + public var fileSharingInternalExpireDateDays: Int = 0 + public var fileSharingRemoteExpireDateEnforced: Bool = false + public var fileSharingRemoteExpireDateDays: Int = 0 + public var fileSharingDefaultPermission: Int = 0 + public var fileSharingDownloadLimit: Bool = false + public var fileSharingDownloadLimitDefaultLimit: Int = 1 + public var themingColor: String = "" + public var themingColorElement: String = "" + public var themingColorText: String = "" + public var themingName: String = "" + public var themingSlogan: String = "" + public var e2EEEnabled: Bool = false + public var e2EEApiVersion: String = "" + public var richDocumentsEnabled: Bool = false + public var richDocumentsMimetypes: [String] = [] + public var activity: [String] = [] + public var notification: [String] = [] + public var filesUndelete: Bool = false + public var filesLockVersion: String = "" // NC 24 + public var filesComments: Bool = false // NC 20 + public var filesBigfilechunking: Bool = false + public var userStatusEnabled: Bool = false + public var externalSites: Bool = false + public var activityEnabled: Bool = false + public var groupfoldersEnabled: Bool = false // NC27 + public var assistantEnabled: Bool = false // NC28 + public var isLivePhotoServerAvailable: Bool = false // NC28 + public var securityGuardDiagnostics = false + public var forbiddenFileNames: [String] = [] + public var forbiddenFileNameBasenames: [String] = [] + public var forbiddenFileNameCharacters: [String] = [] + public var forbiddenFileNameExtensions: [String] = [] + public var recommendations: Bool = false + public var termsOfService: Bool = false public init() {} } From 150087fa32d1d04f02a20c80f7117ed80852269c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 17:39:32 +0200 Subject: [PATCH 12/16] fix Signed-off-by: Marino Faggiana --- .../NextcloudKit+Capabilities.swift | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index 4f69fa6b..3be4411b 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -431,15 +431,6 @@ actor CapabilitiesStore { func set(_ account: String, value: NCCapabilities.Capabilities) { store[account] = value } - - func shouldDisableSharesView(for account: String) -> Bool { - guard let capability = store[account] else { - return true - } - return (!capability.fileSharingApiEnabled && - !capability.filesComments && - capability.activity.isEmpty) - } } /// Singleton container and public API for accessing and caching capabilities. @@ -496,10 +487,6 @@ final public class NCCapabilities: Sendable { // MARK: - Public API - public func disableSharesView(for account: String) async -> Bool { - await store.shouldDisableSharesView(for: account) - } - public func appendCapabilities(for account: String, capabilities: Capabilities) async { await store.set(account, value: capabilities) } @@ -511,28 +498,34 @@ final public class NCCapabilities: Sendable { let group = DispatchGroup() group.enter() - Task { - await store.set(account, value: capabilities) + Task.detached(priority: .utility) { + await self.store.set(account, value: capabilities) group.leave() } group.wait() } - public func getCapabilities(for account: String) async -> Capabilities { - await store.get(account) ?? Capabilities() + public func getCapabilities(for account: String?) async -> Capabilities { + guard let account else { + return Capabilities() + } + return await store.get(account) ?? Capabilities() } /// Synchronously retrieves capabilities for the given account. /// Blocks the current thread until the async actor returns. /// Use only outside the Swift async context (never from another actor or async function). - public func getCapabilitiesBlocking(for account: String) -> Capabilities { + public func getCapabilitiesBlocking(for account: String?) -> Capabilities { + guard let account else { + return Capabilities() + } let group = DispatchGroup() var result: Capabilities? group.enter() - Task { - result = await store.get(account) + Task.detached(priority: .utility) { + result = await self.store.get(account) group.leave() } From 46cfcb852afa29962ab4bcad9d8322498439f65d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 18:22:26 +0200 Subject: [PATCH 13/16] change priority Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Capabilities.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index 3be4411b..14653fb4 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -498,7 +498,7 @@ final public class NCCapabilities: Sendable { let group = DispatchGroup() group.enter() - Task.detached(priority: .utility) { + Task.detached(priority: .userInitiated) { await self.store.set(account, value: capabilities) group.leave() } @@ -524,7 +524,7 @@ final public class NCCapabilities: Sendable { var result: Capabilities? group.enter() - Task.detached(priority: .utility) { + Task.detached(priority: .userInitiated) { result = await self.store.get(account) group.leave() } From 04403414825301a90aa94d7e98850633f61f3e5d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 13 Jun 2025 18:56:46 +0200 Subject: [PATCH 14/16] change name Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Capabilities.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift index 14653fb4..3d5f7a6a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Capabilities.swift +++ b/Sources/NextcloudKit/NextcloudKit+Capabilities.swift @@ -410,9 +410,8 @@ public extension NextcloudKit { capabilities.termsOfService = json.termsOfService?.enabled ?? false // Persist capabilities in shared store - await NCCapabilities.shared.appendCapabilities(for: account, capabilities: capabilities) + await NCCapabilities.shared.appendCapabilitiesAsync(for: account, capabilities: capabilities) return capabilities - } catch { nkLog(error: "Could not decode json capabilities: \(error.localizedDescription)") throw error @@ -487,7 +486,7 @@ final public class NCCapabilities: Sendable { // MARK: - Public API - public func appendCapabilities(for account: String, capabilities: Capabilities) async { + public func appendCapabilitiesAsync(for account: String, capabilities: Capabilities) async { await store.set(account, value: capabilities) } @@ -506,7 +505,7 @@ final public class NCCapabilities: Sendable { group.wait() } - public func getCapabilities(for account: String?) async -> Capabilities { + public func getCapabilitiesAsync(for account: String?) async -> Capabilities { guard let account else { return Capabilities() } From e1181a839b81c9c657687ef7ff16127c0cdb4dbe Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 14 Jun 2025 08:52:12 +0200 Subject: [PATCH 15/16] Update Sources/NextcloudKit/Extensions/Data+Extension.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marcel Müller Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/Extensions/Data+Extension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/Extensions/Data+Extension.swift b/Sources/NextcloudKit/Extensions/Data+Extension.swift index f8131c12..b7d09f6b 100644 --- a/Sources/NextcloudKit/Extensions/Data+Extension.swift +++ b/Sources/NextcloudKit/Extensions/Data+Extension.swift @@ -10,7 +10,7 @@ extension Data { let json = try JSONSerialization.jsonObject(with: self, options: []) let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) guard let jsonString = String(data: data, encoding: .utf8) else { - print("Inavlid data") + print("Invalid data") return } print(jsonString) From 43326a8443261398d1b82d3f86cf15252ada7289 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 14 Jun 2025 08:52:29 +0200 Subject: [PATCH 16/16] Update Sources/NextcloudKit/Extensions/Data+Extension.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marcel Müller Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/Extensions/Data+Extension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/Extensions/Data+Extension.swift b/Sources/NextcloudKit/Extensions/Data+Extension.swift index b7d09f6b..42d62c78 100644 --- a/Sources/NextcloudKit/Extensions/Data+Extension.swift +++ b/Sources/NextcloudKit/Extensions/Data+Extension.swift @@ -24,7 +24,7 @@ extension Data { let json = try JSONSerialization.jsonObject(with: self, options: []) let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) guard let jsonString = String(data: data, encoding: .utf8) else { - print("Inavlid data") + print("Invalid data") return "" } return jsonString