From c8929d307adf540e6032815ccd592f8f2b49c0c1 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Wed, 18 Feb 2026 13:59:25 +0100 Subject: [PATCH 1/2] feat(file-provider): Server features integration in file provider extension - Introduced a new presentFileActions: method in the XPC interface of the main app. - Implemented the presentFileActions: method in the main app to call existing code for file action presentation. - Added the new custom action to the build-time declarations of custom actions in the Info.plist of the file provider extension. - Extended the custom actions handling code switch to process the invocation and call the main app via XPC. Chores: - Created a shared file provider manager object and related stored property for the lifetime of the file provider extension object to be used in various places in its implementation. In example: resolving the user-visible URL of the item to present file actions for. Signed-off-by: Iva Horn --- .../NextcloudFileProviderKit/Package.swift | 2 +- .../Item/Item+Create.swift | 12 ++ .../Item/Item+Fetch.swift | 3 + .../Item/Item+Ignored.swift | 1 + .../Item/Item+LockFile.swift | 1 + .../Item/Item+Modify.swift | 9 ++ .../Item/Item+Trash.swift | 5 + .../Item/Item+Unuploaded.swift | 3 + .../Item+getContextMenuItemTypeFilters.swift | 27 +++++ ...em+typeHasApplicableContextMenuItems.swift | 32 +++++ .../NextcloudFileProviderKit/Item/Item.swift | 17 ++- .../Metadata/SendableItemMetadata+Array.swift | 4 + .../Tests/Interface/Item+Init.swift | 1 + .../Tests/Interface/MockEnumerator.swift | 5 +- .../ItemPropertyTests.swift | 22 ++++ .../FileProviderExtension+CustomActions.swift | 36 +++++- .../FileProviderExtension.swift | 21 +++- .../FileProviderExt/Info.plist | 8 ++ .../FileProviderExt/Services/AppProtocol.h | 12 ++ .../xcshareddata/swiftpm/Package.resolved | 4 +- src/gui/integration/FileActionsWindow.qml | 4 + src/gui/integration/fileactionsmodel.cpp | 114 +++++++++++++++--- src/gui/integration/fileactionsmodel.h | 8 ++ src/gui/macOS/fileproviderservice.h | 9 ++ src/gui/macOS/fileproviderservice.mm | 25 ++++ src/gui/owncloudgui.cpp | 1 + src/gui/systray.cpp | 48 +++++++- src/gui/systray.h | 18 +++ 28 files changed, 417 insertions(+), 35 deletions(-) create mode 100644 shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+getContextMenuItemTypeFilters.swift create mode 100644 shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+typeHasApplicableContextMenuItems.swift diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Package.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Package.swift index 6ad8cb0286fbf..3d322c830661a 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Package.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Package.swift @@ -18,7 +18,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/nextcloud/NextcloudCapabilitiesKit.git", from: "2.4.0"), + .package(url: "https://github.com/nextcloud/NextcloudCapabilitiesKit.git", from: "2.5.0"), .package(url: "https://github.com/nextcloud/NextcloudKit", from: "7.2.3"), .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.55.0"), .package(url: "https://github.com/realm/realm-swift.git", from: "20.0.1"), diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift index 0b49d34dd2984..8a30b5f11ceb6 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift @@ -95,12 +95,15 @@ public extension Item { directory.downloaded = true dbManager.addItemMetadata(directory) + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: directory.contentType) + let fpItem = await Item( metadata: directory, parentItemIdentifier: parentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: log ) @@ -218,12 +221,15 @@ public extension Item { dbManager.addItemMetadata(newMetadata) + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: newMetadata.contentType) + let fpItem = await Item( metadata: newMetadata, parentItemIdentifier: itemTemplate.parentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: log ) @@ -422,12 +428,15 @@ public extension Item { progress.completedUnitCount += 1 + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: bundleRootMetadata.contentType) + return await Item( metadata: bundleRootMetadata, parentItemIdentifier: rootItem.parentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: log ) @@ -597,12 +606,15 @@ public extension Item { ) } + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: itemMetadata.contentType) + item = await Item( metadata: itemMetadata, parentItemIdentifier: parentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: log ) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift index 96e5379ace208..b324a2ba3c5fe 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift @@ -219,12 +219,15 @@ public extension Item { return (nil, nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: itemIdentifier)) } + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: updatedMetadata.contentType) + let fpItem = await Item( metadata: updatedMetadata, parentItemIdentifier: parentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: logger.log ) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Ignored.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Ignored.swift index e22e275eeb918..1159a755ecef0 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Ignored.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Ignored.swift @@ -63,6 +63,7 @@ extension Item { account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: false, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: log ) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift index ab8e158c980e4..4b41af0178826 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift @@ -172,6 +172,7 @@ extension Item { account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: false, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: log ), diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift index b6d18d2e4f585..68f10bd343ad2 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift @@ -78,12 +78,15 @@ public extension Item { ) } + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: newMetadata.contentType) + let modifiedItem = await Item( metadata: newMetadata, parentItemIdentifier: newParentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: logger.log ) @@ -209,12 +212,15 @@ public extension Item { dbManager.addItemMetadata(newMetadata) + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: newMetadata.contentType) + let modifiedItem = await Item( metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: logger.log ) @@ -511,12 +517,15 @@ public extension Item { progress.completedUnitCount += 1 + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: bundleRootMetadata.contentType) + return await Item( metadata: bundleRootMetadata, parentItemIdentifier: parentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: logger.log ) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Trash.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Trash.swift index a14c6b03bbb57..80b6186f0cd77 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Trash.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Trash.swift @@ -30,6 +30,7 @@ extension Item { } let dirtyChildren = dbManager.childItems(directoryMetadata: dirtyMetadata) + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: modifiedItem.remoteInterface, candidate: dirtyMetadata.contentType) let dirtyItem = await Item( metadata: dirtyMetadata, @@ -37,6 +38,7 @@ extension Item { account: account, remoteInterface: modifiedItem.remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: modifiedItem.remoteInterface.supportsTrash(account: account), log: log ) @@ -86,6 +88,7 @@ extension Item { account: account, remoteInterface: modifiedItem.remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: modifiedItem.remoteInterface.supportsTrash(account: account), log: log ) @@ -169,6 +172,7 @@ extension Item { } dbManager.addItemMetadata(restoredItemMetadata) + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: restoredItemMetadata.contentType) return await (Item( metadata: restoredItemMetadata, @@ -176,6 +180,7 @@ extension Item { account: account, remoteInterface: modifiedItem.remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: modifiedItem.remoteInterface.supportsTrash(account: account), log: log ), nil) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Unuploaded.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Unuploaded.swift index fe015ec4bab31..d390b76151c8e 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Unuploaded.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Unuploaded.swift @@ -116,12 +116,15 @@ extension Item { modifiedMetadata.date = newModificationDate } + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: modifiedMetadata.contentType) + return await Item( metadata: modifiedMetadata, parentItemIdentifier: modifiedParentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteInterface.supportsTrash(account: account), log: logger.log ) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+getContextMenuItemTypeFilters.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+getContextMenuItemTypeFilters.swift new file mode 100644 index 0000000000000..75f9355ddbfd0 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+getContextMenuItemTypeFilters.swift @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +extension Item { + /// + /// Gets all MIME type filters from the server capabilities for comparison. + /// + /// - Parameters: + /// - account: The account identifier for the server to check. + /// - remoteInterface: The server proxy object to use. + /// + /// - Returns: An array of strings as provided by NextcloudCapabilitiesKit or an empty array in case of error. + /// + static func getContextMenuItemTypeFilters(account: Account, remoteInterface: RemoteInterface) async -> [String] { + let (_, capabilities, _, capabilitiesError) = await remoteInterface.currentCapabilities(account: account, options: .init(), taskHandler: { _ in }) + + if capabilitiesError == .success { + if let capabilities { + if let apps = capabilities.clientIntegration?.apps { + return apps.flatMap(\.contextMenuItems).flatMap(\.filters) + } + } + } + + return [] + } +} diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+typeHasApplicableContextMenuItems.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+typeHasApplicableContextMenuItems.swift new file mode 100644 index 0000000000000..ef363a39189ff --- /dev/null +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+typeHasApplicableContextMenuItems.swift @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: LGPL-3.0-or-later + +extension Item { + /// + /// Convenience wrapper for ``getContextMenuItemTypeFilters(account:remoteInterface:)`` and ``typeHasApplicableContextMenuItems(filters:candidate:)``. + /// + /// Depending on the call site, it might be more efficient to call both methods individually to avoid redundant capability checks or circumvent boundaries of synchronous and asynchronous contexts. + /// + /// - Parameters: + /// - candidate: The MIME type of the file provider item to check. + /// + /// - Returns: `true`, if the candidate MIME type is covered by the list of filters provided, otherwise `false`. + /// + static func typeHasApplicableContextMenuItems(account: Account, remoteInterface: RemoteInterface, candidate: String) async -> Bool { + let filters = await getContextMenuItemTypeFilters(account: account, remoteInterface: remoteInterface) + return typeHasApplicableContextMenuItems(filters: filters, candidate: candidate) + } + + /// + /// Check whether the MIME type of an item matches any type filter of server-defined context menu items. + /// + /// - Parameters: + /// - filters: A list of MIME type filter strings and prefixes as provided by the server. + /// - candidate: The MIME type of the file provider item to check. + /// + /// - Returns: `true`, if the candidate MIME type is covered by the list of filters provided, otherwise `false`. + /// + static func typeHasApplicableContextMenuItems(filters: [String], candidate: String) -> Bool { + filters.first(where: { candidate.hasPrefix($0) }) != nil + } +} diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item.swift index 5bf4b3d78ade5..92db85fc84658 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item.swift @@ -20,6 +20,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable { public let account: Account public let remoteInterface: RemoteInterface + private let displayFileActions: Bool private let remoteSupportsTrash: Bool public var itemIdentifier: NSFileProviderItemIdentifier { @@ -188,6 +189,8 @@ public final class Item: NSObject, NSFileProviderItem, Sendable { public var userInfo: [AnyHashable: Any]? { var userInfoDict = [AnyHashable: Any]() + userInfoDict["displayFileActions"] = displayFileActions + if metadata.lock { // Can be used to display lock/unlock context menu entries for FPUIActions // Note that only files, not folders, should be lockable/unlockable @@ -199,11 +202,10 @@ public final class Item: NSObject, NSFileProviderItem, Sendable { userInfoDict["displayEvict"] = metadata.downloaded && !metadata.keepDownloaded // https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html - if metadata.permissions.uppercased().contains("R"), // Shareable - ![.rootContainer, .trashContainer].contains(itemIdentifier) - { + if metadata.permissions.uppercased().contains("R") /* Shareable */, ![.rootContainer, .trashContainer].contains(itemIdentifier) { userInfoDict["displayShare"] = true } + return userInfoDict } @@ -265,6 +267,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable { account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: false, remoteSupportsTrash: remoteSupportsTrash, log: log ) @@ -308,6 +311,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable { account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: false, remoteSupportsTrash: remoteSupportsTrash, log: log ) @@ -321,6 +325,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable { account: Account, remoteInterface: RemoteInterface, dbManager: FilesDatabaseManager, + displayFileActions: Bool, remoteSupportsTrash: Bool, log: any FileProviderLogging ) { @@ -330,6 +335,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable { logger = FileProviderLogger(category: "Item", log: log) self.remoteInterface = remoteInterface self.dbManager = dbManager + self.displayFileActions = displayFileActions self.remoteSupportsTrash = remoteSupportsTrash super.init() } @@ -382,12 +388,17 @@ public final class Item: NSObject, NSFileProviderItem, Sendable { return nil } + // Display File Actions + + let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: metadata.contentType) + return Item( metadata: metadata, parentItemIdentifier: parentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteSupportsTrash, log: log ) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift index 5752d354f3e27..b39be8f6b99eb 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift @@ -10,6 +10,7 @@ extension [SendableItemMetadata] { func toFileProviderItems(account: Account, remoteInterface: RemoteInterface, dbManager: FilesDatabaseManager, log: any FileProviderLogging) async throws -> [Item] { let logger = FileProviderLogger(category: "toFileProviderItems", log: log) let remoteSupportsTrash = await remoteInterface.supportsTrash(account: account) + let allFilters = await Item.getContextMenuItemTypeFilters(account: account, remoteInterface: remoteInterface) return try await concurrentChunkedCompactMap { (itemMetadata: SendableItemMetadata) -> Item? in guard !itemMetadata.e2eEncrypted else { @@ -28,12 +29,15 @@ extension [SendableItemMetadata] { throw FilesDatabaseManager.parentMetadataNotFoundError(itemUrl: targetUrl) } + let displayFileActions = Item.typeHasApplicableContextMenuItems(filters: allFilters, candidate: itemMetadata.contentType) + let item = Item( metadata: itemMetadata, parentItemIdentifier: parentItemIdentifier, account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: displayFileActions, remoteSupportsTrash: remoteSupportsTrash, log: log ) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/Item+Init.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/Item+Init.swift index eb2f15422dd3b..96a01317c2494 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/Item+Init.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/Item+Init.swift @@ -20,6 +20,7 @@ public extension Item { account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/MockEnumerator.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/MockEnumerator.swift index 67aadd9becc0a..a850477b1b712 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/MockEnumerator.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/MockEnumerator.swift @@ -20,9 +20,7 @@ public class MockEnumerator: NSObject, NSFileProviderEnumerator { self.remoteInterface = remoteInterface } - public func enumerateItems( - for observer: any NSFileProviderEnumerationObserver, startingAt _: NSFileProviderPage - ) { + public func enumerateItems(for observer: any NSFileProviderEnumerationObserver, startingAt _: NSFileProviderPage) { let remoteSupportsTrash = remoteInterface.directMockCapabilities()?.files?.undelete ?? false var items: [Item] = [] for item in enumeratorItems { @@ -36,6 +34,7 @@ public class MockEnumerator: NSObject, NSFileProviderEnumerator { account: account, remoteInterface: remoteInterface, dbManager: dbManager, + displayFileActions: false, remoteSupportsTrash: remoteSupportsTrash, log: FileProviderLogMock() ) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemPropertyTests.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemPropertyTests.swift index 12b30c9ef885a..2e399181de9c9 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemPropertyTests.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemPropertyTests.swift @@ -365,6 +365,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: remoteInterface, dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: remoteSupportsTrash, log: FileProviderLogMock() ) @@ -426,6 +427,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -452,6 +454,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -475,6 +478,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -495,6 +499,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -514,6 +519,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -534,6 +540,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -549,6 +556,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: false, log: FileProviderLogMock() ) @@ -568,6 +576,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -586,6 +595,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -607,6 +617,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -625,6 +636,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -643,6 +655,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -666,6 +679,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -684,6 +698,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -705,6 +720,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -723,6 +739,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -741,6 +758,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -761,6 +779,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -793,6 +812,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -822,6 +842,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) @@ -846,6 +867,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase { account: Self.account, remoteInterface: MockRemoteInterface(account: Self.account), dbManager: Self.dbManager, + displayFileActions: false, remoteSupportsTrash: true, log: FileProviderLogMock() ) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift index e5838d51e29c6..78e7a417564a3 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift @@ -12,6 +12,40 @@ extension FileProviderExtension: NSFileProviderCustomAction { completionHandler: @escaping ((any Error)?) -> Void ) -> Progress { switch actionIdentifier.rawValue { + case "com.nextcloud.desktopclient.FileProviderExt.FileActionsAction": + guard let itemIdentifier = itemIdentifiers.first else { + logger.error("Failed to get first item identifier for file actions action.") + completionHandler(NSFileProviderError(.noSuchItem)) + return Progress() + } + + guard let dbManager else { + logger.error("Cannot fetch metadata for item file actions due to database manager not being available.", [.item: itemIdentifier]) + completionHandler(NSFileProviderError(.cannotSynchronize)) + return Progress() + } + + Task { + guard let userVisibleURL = try await manager?.getUserVisibleURL(for: itemIdentifier) else { + logger.error("Failed to get user-visible URL for item.", [.item: itemIdentifier]) + completionHandler(NSFileProviderError(.noSuchItem)) + return + } + + guard let metadata = dbManager.itemMetadata(itemIdentifier) else { + logger.error("Failed to get metadata for item.", [.item: itemIdentifier]) + completionHandler(NSFileProviderError(.cannotSynchronize)) + return + } + + let path = userVisibleURL.path + let domainIdentifier = domain.identifier.rawValue + logger.info("Telling main app to present file actions.", [.item: path, .domain: domainIdentifier]) + app?.presentFileActions(metadata.ocId, path: path, remoteItemPath: metadata.path, withDomainIdentifier: domainIdentifier) + completionHandler(nil) + } + + return Progress() case "com.nextcloud.desktopclient.FileProviderExt.KeepDownloadedAction": return performKeepDownloadedAction( keepDownloaded: true, @@ -30,7 +64,7 @@ extension FileProviderExtension: NSFileProviderCustomAction { return Progress() } } - + private func performKeepDownloadedAction( keepDownloaded: Bool, onItemsWithIdentifiers itemIdentifiers: [NSFileProviderItemIdentifier], diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift index faf49bdec593c..35bce1526acda 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift @@ -11,12 +11,20 @@ import OSLog /// The file provider replicated extension implementation. /// @objc final class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, @unchecked Sendable { + /// + /// The file provider domain managed by this file provider extension implementation. + /// let domain: NSFileProviderDomain let keychain: Keychain let log: any FileProviderLogging let logger: FileProviderLogger + /// + /// The file provider manager for the domain managed by this extension implementation. + /// + let manager: NSFileProviderManager? + // MARK: XPC /// @@ -81,6 +89,7 @@ import OSLog // application extension process, call `FileProviderExtension.init(domain:)` to instantiate // the extension for that domain, and call methods on the instance. self.domain = domain + self.manager = NSFileProviderManager(for: domain) // Set up logging. self.log = FileProviderLog(fileProviderDomainIdentifier: domain.identifier) @@ -514,13 +523,13 @@ import OSLog return } - guard let fpManager = NSFileProviderManager(for: domain) else { + guard let manager = manager else { logger.error("Could not get file provider manager for domain: \(self.domain.displayName)") completionHandler() return } - let materialisedEnumerator = fpManager.enumeratorForMaterializedItems() + let materialisedEnumerator = manager.enumeratorForMaterializedItems() let materialisedObserver = MaterializedEnumerationObserver(account: ncAccount, dbManager: dbManager, log: log) { _, _ in completionHandler() } @@ -532,12 +541,12 @@ import OSLog // MARK: - Helper functions func signalEnumerator(completionHandler: @escaping (_ error: Error?) -> Void) { - guard let fpManager = NSFileProviderManager(for: domain) else { + guard let manager = manager else { logger.error("Could not get file provider manager for domain, could not signal enumerator. This might lead to future conflicts.") return } - fpManager.signalEnumerator(for: .workingSet, completionHandler: completionHandler) + manager.signalEnumerator(for: .workingSet, completionHandler: completionHandler) } @objc func sendFileProviderDomainIdentifier() { @@ -548,14 +557,14 @@ import OSLog } private func signalEnumeratorAfterAccountSetup() { - guard let fpManager = NSFileProviderManager(for: domain) else { + guard let manager = manager else { logger.error("Could not get file provider manager for domain \(self.domain.displayName), cannot notify after account setup") return } assert(ncAccount != nil) - fpManager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in + manager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in if error != nil { self.logger.error("Error resolving not authenticated, received error: \(error!.localizedDescription)") } diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist index 853d3bb257e44..616bcdd392ce1 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist @@ -41,6 +41,14 @@ NSExtensionFileProviderActionName Allow automatic freeing up space + + NSExtensionFileProviderActionActivationRule + SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayFileActions == true ).@count > 0 + NSExtensionFileProviderActionIdentifier + com.nextcloud.desktopclient.FileProviderExt.FileActionsAction + NSExtensionFileProviderActionName + File actions + NSExtensionFileProviderDocumentGroup $(DEVELOPMENT_TEAM).$(OC_APPLICATION_REV_DOMAIN) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/AppProtocol.h b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/AppProtocol.h index 08569ab826f6d..9e8f599b12be8 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/AppProtocol.h +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/AppProtocol.h @@ -7,12 +7,22 @@ #define AppProtocol_h #import +NS_ASSUME_NONNULL_BEGIN /** * @brief The main app APIs exposed through XPC. */ @protocol AppProtocol +/** + * @brief The file provider extension can tell the main app to offer the user server-features for the given item. + * @param fileId The ocId as provided by the server for item identification independent from path. + * @param path The local and absolute path for the item to offer actions for. + * @param remoteItemPath The server-side path of the item, used as a fallback when no sync folder is configured. + * @param domainIdentifier The file provider domain identifier for the account that manages this file. + */ +- (void)presentFileActions:(NSString *)fileId path:(NSString *)path remoteItemPath:(NSString *)remoteItemPath withDomainIdentifier:(NSString *)domainIdentifier; + /** * @brief The file provider extension can report its synchronization status as a string constant value to the main app through this method. * @param status The synchronization status string. @@ -22,4 +32,6 @@ @end +NS_ASSUME_NONNULL_END #endif /* AppProtocol_h */ + diff --git a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 42f4f740055f7..27644534f27e3 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nextcloud/NextcloudCapabilitiesKit.git", "state" : { - "revision" : "296f28db1bb02c51f215e7ac34430885f5046058", - "version" : "2.4.6" + "revision" : "e7cc7d2214ff565e08cffd8495465634d2c6998c", + "version" : "2.5.0" } }, { diff --git a/src/gui/integration/FileActionsWindow.qml b/src/gui/integration/FileActionsWindow.qml index e526fbff6dc25..19e01f242f3e4 100644 --- a/src/gui/integration/FileActionsWindow.qml +++ b/src/gui/integration/FileActionsWindow.qml @@ -25,6 +25,8 @@ ApplicationWindow { property var accountState: ({}) property string localPath: "" property string shortLocalPath: "" + property string fileId: "" + property string remoteItemPath: "" readonly property int windowRadius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius @@ -34,6 +36,8 @@ ApplicationWindow { id: fileActionModel accountState: root.accountState localPath: root.localPath + fileId: root.fileId + remoteItemPath: root.remoteItemPath } background: Rectangle { diff --git a/src/gui/integration/fileactionsmodel.cpp b/src/gui/integration/fileactionsmodel.cpp index 93404a063da6b..4a2f504eb0e71 100644 --- a/src/gui/integration/fileactionsmodel.cpp +++ b/src/gui/integration/fileactionsmodel.cpp @@ -116,24 +116,71 @@ QByteArray FileActionsModel::fileId() const return _fileId; } -void FileActionsModel::setupFileProperties() +void FileActionsModel::setFileId(const QByteArray &fileId) { - const auto folderForPath = FolderMan::instance()->folderForPath(_localPath); - _filePath = _localPath.mid(folderForPath->cleanPath().length() + 1); - SyncJournalFileRecord fileRecord; - if (!folderForPath->journalDb()->getFileRecord(_filePath, &fileRecord)) { - qCWarning(lcFileActions) << "Invalid file record for path:" << _localPath; + if (fileId == _fileId) { return; } - _fileId = fileRecord._fileId; + _fileId = fileId; + + if (_accountState && !_localPath.isEmpty()) { + parseEndpoints(); + } + + Q_EMIT fileChanged(); +} + +QString FileActionsModel::remoteItemPath() const +{ + return _remoteItemPath; +} + +void FileActionsModel::setRemoteItemPath(const QString &remoteItemPath) +{ + if (remoteItemPath == _remoteItemPath) { + return; + } + _remoteItemPath = remoteItemPath; + Q_EMIT fileChanged(); +} + +void FileActionsModel::setupFileProperties() +{ + qCDebug(lcFileActions) << "Setting up file properties for:" << _localPath; + + const auto folderForPath = FolderMan::instance()->folderForPath(_localPath); + + // Declare once so it's available after the if-else clause. + QMimeDatabase::MatchMode mimeMatchMode; + + if (folderForPath) { // Synchronization folders + qCDebug(lcFileActions) << "Found synchronization folder for" << _localPath; + _filePath = _localPath.mid(folderForPath->cleanPath().length() + 1); + + SyncJournalFileRecord fileRecord; + + if (!folderForPath->journalDb()->getFileRecord(_filePath, &fileRecord)) { + qCWarning(lcFileActions) << "Invalid file record for path:" << _localPath; + return; + } + + _fileId = fileRecord._fileId; + + // Decide match mode based on whether this is a virtual file + mimeMatchMode = fileRecord.isVirtualFile() ? QMimeDatabase::MatchExtension + : QMimeDatabase::MatchDefault; + } else { // Virtual file systems + qCDebug(lcFileActions) << "Did not find synchronization folder for" << _localPath; + // In this case, _fileId should already be initialized with a value from the calling code. + _filePath = _localPath; + // When we don't have a sync folder, use extension matching + mimeMatchMode = QMimeDatabase::MatchExtension; + } - const auto mimeMatchMode = fileRecord.isVirtualFile() ? QMimeDatabase::MatchExtension - : QMimeDatabase::MatchDefault; QMimeDatabase mimeDb; const auto mimeType = mimeDb.mimeTypeForFile(_localPath, mimeMatchMode); _mimeType = mimeType; - _fileIcon = _accountUrl + Activity::relativeServerFileTypeIconPath(_mimeType); } @@ -175,11 +222,25 @@ void FileActionsModel::setResponse(const Response &response) void FileActionsModel::parseEndpoints() { + auto resetActions = [this](const ActionList &actions) { + beginResetModel(); + _fileActions = actions; + endResetModel(); + Q_EMIT fileActionModelChanged(); + }; + + if (!_accountState) { + qCWarning(lcFileActions) << "No account state available for" << _localPath; + resetActions({}); + return; + } + if (!_accountState->isConnected()) { qCWarning(lcFileActions) << "The account is not connected" << _accountUrl; setResponse({ tr("Your account is offline %1.", "account url").arg(_accountUrl), _accountUrl }); + resetActions({}); return; } @@ -196,6 +257,7 @@ void FileActionsModel::parseEndpoints() << _localPath; setResponse({ tr("The file type for %1 is not valid.", "file name").arg(_localPath), _accountUrl }); + resetActions({}); return; } @@ -206,9 +268,11 @@ void FileActionsModel::parseEndpoints() setResponse({ tr("No file actions were returned by the server for %1 files.", "file mymetype") .arg(_mimeType.filterString()), _accountUrl }); + resetActions({}); return; } + ActionList actions; for (const auto &contextMenu : contextMenuList) { QueryList queryList; const auto paramsMap = contextMenu.value("params").toMap(); @@ -228,17 +292,18 @@ void FileActionsModel::parseEndpoints() } } - _fileActions.append({ parseIcon(contextMenu.value("icon").toString()), - contextMenu.value("name").toString(), - contextMenu.value("url").toString(), - contextMenu.value("method").toString(), - queryList }); + actions.append({ parseIcon(contextMenu.value("icon").toString()), + contextMenu.value("name").toString(), + contextMenu.value("url").toString(), + contextMenu.value("method").toString(), + queryList }); } + resetActions(actions); + qCDebug(lcFileActions) << "File" << _localPath << "has" - << _fileActions.size() + << actions.size() << "actions available."; - Q_EMIT fileActionModelChanged(); } QString FileActionsModel::parseUrl(const QString &url) const @@ -297,14 +362,25 @@ void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode) const auto root = json.object().value(QStringLiteral("root")).toObject(); const auto folderForPath = FolderMan::instance()->folderForPath(_localPath); - const auto remoteFolderPath = _accountUrl + folderForPath->remotePath(); const auto successMessage = tr("%1 done.", "file action success message").arg(fileAction); + + QString remoteFolderPath; + if (folderForPath) { + remoteFolderPath = _accountUrl + folderForPath->remotePath(); + } else if (!_remoteItemPath.isEmpty()) { + remoteFolderPath = _accountUrl + _remoteItemPath; + } else { + qCWarning(lcFileActions) << "Failed to find folder for path and no remote item path available:" << _localPath; + return; + } + if (root.empty()) { setResponse({ successMessage, remoteFolderPath }); return; } const auto rows = root.value(QStringLiteral("rows")).toArray(); + if (rows.empty()) { setResponse({ successMessage, remoteFolderPath }); return; @@ -313,6 +389,7 @@ void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode) for (const auto &rowValue : rows) { const auto row = rowValue.toObject(); const auto children = row.value("children").toArray(); + for (const auto &childValue : children) { const auto child = childValue.toObject(); setResponse({ child.value(QStringLiteral("element")).toString(), @@ -322,3 +399,4 @@ void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode) } } // namespace OCC + diff --git a/src/gui/integration/fileactionsmodel.h b/src/gui/integration/fileactionsmodel.h index 222478a62479d..b0354bdd74cd3 100644 --- a/src/gui/integration/fileactionsmodel.h +++ b/src/gui/integration/fileactionsmodel.h @@ -19,6 +19,8 @@ class FileActionsModel : public QAbstractListModel { Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY fileChanged) + Q_PROPERTY(QByteArray fileId READ fileId WRITE setFileId NOTIFY fileChanged) + Q_PROPERTY(QString remoteItemPath READ remoteItemPath WRITE setRemoteItemPath NOTIFY fileChanged) Q_PROPERTY(QString fileIcon READ fileIcon NOTIFY fileChanged) Q_PROPERTY(QString responseLabel READ responseLabel WRITE setResponseLabel NOTIFY responseChanged) Q_PROPERTY(QString responseUrl READ responseUrl WRITE setResponseUrl NOTIFY responseChanged) @@ -67,6 +69,11 @@ class FileActionsModel : public QAbstractListModel { void setLocalPath(const QString &localPath); [[nodiscard]] QByteArray fileId() const; + void setFileId(const QByteArray &fileId); + + [[nodiscard]] QString remoteItemPath() const; + void setRemoteItemPath(const QString &remoteItemPath); + [[nodiscard]] QMimeType mimeType() const; [[nodiscard]] QString fileIcon() const; void setupFileProperties(); @@ -99,6 +106,7 @@ public slots: AccountState *_accountState; QString _localPath; QByteArray _fileId; + QString _remoteItemPath; QMimeType _mimeType; QString _filePath; QString _accountUrl; diff --git a/src/gui/macOS/fileproviderservice.h b/src/gui/macOS/fileproviderservice.h index 98fe8ba9334ff..dab535181b893 100644 --- a/src/gui/macOS/fileproviderservice.h +++ b/src/gui/macOS/fileproviderservice.h @@ -59,6 +59,15 @@ class FileProviderService : public QObject */ void syncStateChanged(const AccountPtr &account, SyncResult::Status state); + /** + * @brief Emitted when a file provider extension requests to show the file actions dialog. + * @param fileId The ocId as provided by the server for item identification independent from path. + * @param localFile The local file path for which to show actions. + * @param remoteItemPath The server-side path of the item, used as a fallback when no sync folder is configured. + * @param fileProviderDomainIdentifier The file provider domain identifier (optional, empty if not provided). + */ + void showFileActionsDialog(const QString &fileId, const QString &localFile, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier); + private: class MacImplementation; std::unique_ptr d; diff --git a/src/gui/macOS/fileproviderservice.mm b/src/gui/macOS/fileproviderservice.mm index 5840f44619b09..0b2c97e9171cd 100644 --- a/src/gui/macOS/fileproviderservice.mm +++ b/src/gui/macOS/fileproviderservice.mm @@ -30,6 +30,31 @@ @interface FileProviderServiceDelegate : NSObject @implementation FileProviderServiceDelegate +- (void)presentFileActions:(NSString *)fileId path:(NSString *)path remoteItemPath:(NSString *)remoteItemPath withDomainIdentifier:(NSString *)domainIdentifier +{ + qCDebug(OCC::lcMacFileProviderService) << "Should present file actions for item with fileId:" + << fileId + << "and path:" + << path + << "remote item path:" + << remoteItemPath + << "domain identifier:" + << domainIdentifier; + + const auto qFileId = QString::fromNSString(fileId); + const auto localPath = QString::fromNSString(path); + const auto qRemoteItemPath = QString::fromNSString(remoteItemPath); + const auto domainId = QString::fromNSString(domainIdentifier); + + // Use QMetaObject::invokeMethod to emit the signal on the correct thread + // since this callback may be called on an XPC dispatch queue (non-main thread) + QMetaObject::invokeMethod(_service, "showFileActionsDialog", Qt::QueuedConnection, + Q_ARG(QString, qFileId), + Q_ARG(QString, localPath), + Q_ARG(QString, qRemoteItemPath), + Q_ARG(QString, domainId)); +} + - (void)reportSyncStatus:(NSString *)status forDomainIdentifier:(NSString *)domainIdentifier { const auto statusString = QString::fromNSString(status); diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 1fe337b4433d4..2f1b3bd0214c8 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -117,6 +117,7 @@ ownCloudGui::ownCloudGui(Application *parent) #ifdef BUILD_FILE_PROVIDER_MODULE connect(Mac::FileProvider::instance()->service(), &Mac::FileProviderService::syncStateChanged, this, &ownCloudGui::slotComputeOverallSyncStatus); + connect(Mac::FileProvider::instance()->service(), &Mac::FileProviderService::showFileActionsDialog, _tray.data(), &Systray::slotShowFileProviderFileActionsDialog); #endif connect(Logger::instance(), &Logger::guiLog, this, &ownCloudGui::slotShowTrayMessage); diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 39ba5a42b66e4..89d07bde3f5fc 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -441,6 +441,32 @@ void Systray::showFileActionsDialog(const QString &localPath) createFileActionsDialog(localPath); } +#ifdef BUILD_FILE_PROVIDER_MODULE + +void Systray::slotShowFileProviderFileActionsDialog(const QString &fileId, const QString &localPath, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier) +{ + createFileProviderFileActionsDialog(fileId, localPath, remoteItemPath, fileProviderDomainIdentifier); +} + +void Systray::createFileProviderFileActionsDialog(const QString &fileId, const QString &localPath, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier) +{ + if (!_trayEngine) { + qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath << "as no tray engine was available"; + return; + } + + const auto accountState = AccountManager::instance()->accountFromFileProviderDomainIdentifier(fileProviderDomainIdentifier); + if (!accountState) { + qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath + << "no account found for domain identifier" << fileProviderDomainIdentifier; + return; + } + + createFileActionsDialogWithAccountState(localPath, accountState.data(), fileId, remoteItemPath); +} + +#endif + void Systray::createFileActionsDialog(const QString &localPath) { if (!_trayEngine) { @@ -454,7 +480,23 @@ void Systray::createFileActionsDialog(const QString &localPath) return; } + createFileActionsDialogWithAccountState(localPath, folder->accountState()); +} + +void Systray::createFileActionsDialogWithAccountState(const QString &localPath, AccountState *accountState, const QString &fileId, const QString &remoteItemPath) +{ + if (!_trayEngine) { + qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath << "as no tray engine was available"; + return; + } + + if (!accountState) { + qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath << "no account state provided"; + return; + } + QQmlComponent fileActionsQml(trayEngine(), QStringLiteral("qrc:/qml/src/gui/integration/FileActionsWindow.qml")); + if (fileActionsQml.isError()) { qCWarning(lcSystray) << fileActionsQml.errorString(); qCWarning(lcSystray) << fileActionsQml.errors(); @@ -463,14 +505,18 @@ void Systray::createFileActionsDialog(const QString &localPath) QFileInfo localFile{localPath}; const auto shortLocalPath = localFile.fileName(); + const QVariantMap initialProperties{ - {"accountState", QVariant::fromValue(folder->accountState())}, + {"accountState", QVariant::fromValue(accountState)}, + {"fileId", fileId}, + {"remoteItemPath", remoteItemPath}, {"shortLocalPath", shortLocalPath}, {"localPath", localPath}, }; const auto fileActionsDialog = fileActionsQml.createWithInitialProperties(initialProperties); const auto dialog = qobject_cast(fileActionsDialog); + if (!dialog) { qCWarning(lcSystray) << "File Actions dialog window resulted in creation of object that was not a window!"; return; diff --git a/src/gui/systray.h b/src/gui/systray.h index 4cc3e4b0a2b65..b48d2dbfc3ff1 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -148,6 +148,12 @@ public slots: void createFileActivityDialog(const QString &localPath); void showFileActionsDialog(const QString &localPath); + #ifdef BUILD_FILE_PROVIDER_MODULE + + void slotShowFileProviderFileActionsDialog(const QString &fileId, const QString &localPath, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier); + + #endif + void presentShareViewInTray(const QString &localPath); void presentFileActionsViewInSystray(const QString &localPath); @@ -169,6 +175,18 @@ private slots: void createFileDetailsDialog(const QString &localPath); void createFileActionsDialog(const QString &localPath); + #ifdef BUILD_FILE_PROVIDER_MODULE + + void createFileProviderFileActionsDialog(const QString &fileId, const QString &localPath, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier); + + #endif + + /** + * @param fileId This is optional and null by default. Only when there are no synchronization folders configured (for example when only a file provider is used) is this value required. + * @param remoteItemPath The server-side path of the item. Used as a fallback to determine the success response URL when no sync folder is configured. + */ + void createFileActionsDialogWithAccountState(const QString &localPath, AccountState *accountState, const QString &fileId = {}, const QString &remoteItemPath = {}); + [[nodiscard]] QScreen *currentScreen() const; [[nodiscard]] QRect currentScreenRect() const; [[nodiscard]] QRect currentAvailableScreenRect() const; From 5eeebfa4e48075614f1d424c3c3c2ebde7b40460 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 19 Feb 2026 17:51:55 +0100 Subject: [PATCH 2/2] fix(macOS): FinderSync socket resolution and logging Signed-off-by: Iva Horn --- .../FinderSyncExt/FinderSync.m | 63 ++++++++++++++++--- .../FinderSyncSocketLineProcessor.m | 55 ++++++++++++---- .../FinderSyncExt/Info.plist | 2 + 3 files changed, 99 insertions(+), 21 deletions(-) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m index 7c389f8a47263..f52d279ec4656 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m +++ b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m @@ -5,6 +5,7 @@ */ #import "FinderSync.h" +#import @interface FinderSync() { @@ -13,9 +14,21 @@ @interface FinderSync() NSMutableDictionary *_strings; NSMutableArray *_menuItems; NSCondition *_menuIsComplete; + os_log_t _log; } @end +static os_log_t getFinderSyncLogger(void) { + static dispatch_once_t onceToken; + static os_log_t logger = NULL; + dispatch_once(&onceToken, ^{ + NSBundle *bundle = [NSBundle bundleForClass:[FinderSync class]]; + NSString *subsystem = bundle.bundleIdentifier ?: @"FinderSyncExt"; + logger = os_log_create(subsystem.UTF8String, "FinderSync"); + }); + return logger; +} + @implementation FinderSync - (instancetype)init @@ -23,10 +36,12 @@ - (instancetype)init self = [super init]; if (self) { + _log = getFinderSyncLogger(); + os_log_debug(_log, "Initializing FinderSync extension"); FIFinderSyncController *syncController = [FIFinderSyncController defaultController]; NSBundle *extBundle = [NSBundle bundleForClass:[self class]]; // This was added to the bundle's Info.plist to get it from the build system - NSString *socketApiPrefix = [extBundle objectForInfoDictionaryKey:@"SocketApiPrefix"]; + NSString *groupIdentifier = [extBundle objectForInfoDictionaryKey:@"NCApplicationGroupIdentifier"]; NSImage *ok = [extBundle imageForResource:@"ok.icns"]; NSImage *ok_swm = [extBundle imageForResource:@"ok_swm.icns"]; @@ -45,27 +60,29 @@ - (instancetype)init [syncController setBadgeImage:warning label:@"Ignored" forBadgeIdentifier:@"IGNORE+SWM"]; [syncController setBadgeImage:error label:@"Error" forBadgeIdentifier:@"ERROR+SWM"]; - NSURL *container = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:socketApiPrefix]; + NSURL *container = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupIdentifier]; NSURL *library = [container URLByAppendingPathComponent:@"Library" isDirectory:true]; NSURL *applicationSupport = [library URLByAppendingPathComponent:@"Application Support" isDirectory:true]; NSURL *socketPath = [applicationSupport URLByAppendingPathComponent:@"s" isDirectory:NO]; - NSLog(@"Socket path: %@", socketPath.path); + os_log_debug(_log, "Socket path: %{public}@", socketPath.path); if (socketPath.path) { + os_log_debug(_log, "Socket path determined: %{public}@", socketPath.path); self.lineProcessor = [[FinderSyncSocketLineProcessor alloc] initWithDelegate:self]; self.localSocketClient = [[LocalSocketClient alloc] initWithSocketPath:socketPath.path lineProcessor:self.lineProcessor]; [self.localSocketClient start]; [self.localSocketClient askOnSocket:@"" query:@"GET_STRINGS"]; } else { - NSLog(@"No socket path. Not initiating local socket client."); + os_log_error(_log, "No socket path available. Not initiating local socket client."); self.localSocketClient = nil; } _registeredDirectories = NSMutableSet.set; _strings = NSMutableDictionary.dictionary; _menuIsComplete = [[NSCondition alloc] init]; + os_log_debug(_log, "FinderSync extension initialization completed"); } return self; @@ -75,20 +92,23 @@ - (instancetype)init - (void)requestBadgeIdentifierForURL:(NSURL *)url { + os_log_debug(_log, "Requesting badge identifier for URL: %{public}@", url.path); BOOL isDir; if ([[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory: &isDir] == NO) { - NSLog(@"ERROR: Could not determine file type of %@", [url path]); + os_log_error(_log, "Could not determine file type of %{public}@", [url path]); isDir = NO; } NSString* normalizedPath = [[url path] decomposedStringWithCanonicalMapping]; [self.localSocketClient askForIcon:normalizedPath isDirectory:isDir]; + os_log_debug(_log, "Badge identifier request completed for: %{public}@", normalizedPath); } #pragma mark - Menu and toolbar item support - (NSString*) selectedPathsSeparatedByRecordSeparator { + os_log_debug(_log, "Building selected paths string with record separators"); FIFinderSyncController *syncController = [FIFinderSyncController defaultController]; NSMutableString *string = [[NSMutableString alloc] init]; [syncController.selectedItemURLs enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) { @@ -98,19 +118,24 @@ - (NSString*) selectedPathsSeparatedByRecordSeparator NSString* normalizedPath = [[obj path] decomposedStringWithCanonicalMapping]; [string appendString:normalizedPath]; }]; + os_log_debug(_log, "Selected paths string built: %lu paths", (unsigned long)syncController.selectedItemURLs.count); return string; } - (void)waitForMenuToArrive { + os_log_debug(_log, "Waiting for menu to arrive"); [self->_menuIsComplete lock]; [self->_menuIsComplete wait]; [self->_menuIsComplete unlock]; + os_log_debug(_log, "Menu arrival wait completed"); } - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu { + os_log_debug(_log, "Building menu for menu kind: %lu", (unsigned long)whichMenu); if(![self.localSocketClient isConnected]) { + os_log_error(_log, "Local socket client not connected, cannot build menu"); return nil; } @@ -131,6 +156,7 @@ - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu *stop = YES; } }]; + os_log_debug(_log, "Root directories check: onlyRootsSelected = %d", onlyRootsSelected); NSString *paths = [self selectedPathsSeparatedByRecordSeparator]; [self.localSocketClient askOnSocket:paths query:@"GET_MENU_ITEMS"]; @@ -141,6 +167,7 @@ - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu id contextMenuTitle = [_strings objectForKey:@"CONTEXT_MENU_TITLE"]; if (contextMenuTitle && !onlyRootsSelected) { + os_log_debug(_log, "Creating context menu with title: %{public}@", contextMenuTitle); NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; NSMenu *subMenu = [[NSMenu alloc] initWithTitle:@""]; NSMenuItem *subMenuItem = [menu addItemWithTitle:contextMenuTitle action:nil keyEquivalent:@""]; @@ -162,70 +189,88 @@ - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu } idx++; } + os_log_debug(_log, "Context menu created with %d items", idx); return menu; } + os_log_debug(_log, "No context menu created: contextMenuTitle=%@, onlyRootsSelected=%d", contextMenuTitle != nil ? @"present" : @"absent", onlyRootsSelected); return nil; } - (void)subMenuActionClicked:(id)sender { long idx = [(NSMenuItem*)sender tag]; + os_log_debug(_log, "Menu item clicked at index: %ld", idx); NSString *command = [[_menuItems objectAtIndex:idx] valueForKey:@"command"]; NSString *paths = [self selectedPathsSeparatedByRecordSeparator]; + os_log_debug(_log, "Executing command: %{public}@", command); [self.localSocketClient askOnSocket:paths query:command]; + os_log_debug(_log, "Command execution completed"); } #pragma mark - SyncClientProxyDelegate implementation - (void)setResult:(NSString *)result forPath:(NSString*)path { + os_log_debug(_log, "Setting result: %{public}@ for path: %{public}@", result, path); NSString *const normalizedPath = path.decomposedStringWithCanonicalMapping; NSURL *const urlForPath = [NSURL fileURLWithPath:normalizedPath]; if (urlForPath == nil) { + os_log_error(_log, "Failed to create URL for path: %{public}@", normalizedPath); return; } [FIFinderSyncController.defaultController setBadgeIdentifier:result forURL:urlForPath]; + os_log_debug(_log, "Badge identifier set successfully"); } - (void)reFetchFileNameCacheForPath:(NSString*)path { - + os_log_debug(_log, "Refetching file name cache for path: %{public}@", path); } - (void)registerPath:(NSString*)path { + os_log_debug(_log, "Registering path: %{public}@", path); NSAssert(_registeredDirectories, @"Registered directories should be a valid set!"); [_registeredDirectories addObject:[NSURL fileURLWithPath:path]]; [FIFinderSyncController defaultController].directoryURLs = _registeredDirectories; + os_log_debug(_log, "Path registration completed"); } - (void)unregisterPath:(NSString*)path { + os_log_debug(_log, "Unregistering path: %{public}@", path); [_registeredDirectories removeObject:[NSURL fileURLWithPath:path]]; [FIFinderSyncController defaultController].directoryURLs = _registeredDirectories; + os_log_debug(_log, "Path unregistration completed"); } - (void)setString:(NSString*)key value:(NSString*)value { + os_log_debug(_log, "Setting string: %{public}@ = %{public}@", key, value); [_strings setObject:value forKey:key]; } - (void)resetMenuItems { + os_log_debug(_log, "Resetting menu items"); _menuItems = [[NSMutableArray alloc] init]; + os_log_debug(_log, "Menu items reset completed"); } - (void)addMenuItem:(NSDictionary *)item { - NSLog(@"Adding menu item."); + os_log_debug(_log, "Adding menu item with title: %{public}@", [item valueForKey:@"text"] ?: @"(no title)"); [_menuItems addObject:item]; + os_log_debug(_log, "Menu item added, total items: %lu", (unsigned long)_menuItems.count); } - (void)menuHasCompleted { - NSLog(@"Emitting menu is complete signal now."); + os_log_debug(_log, "Menu completion signal received"); [self->_menuIsComplete signal]; + os_log_debug(_log, "Menu signal emitted"); } - (void)connectionDidDie { + os_log_error(_log, "Connection to sync client died"); [_strings removeAllObjects]; [_registeredDirectories removeAllObjects]; // For some reason the FIFinderSync cache doesn't seem to be cleared for the root item when @@ -235,7 +280,9 @@ - (void)connectionDidDie // This will tell Finder that this extension isn't attached to any directory // until we can reconnect to the sync client. [FIFinderSyncController defaultController].directoryURLs = nil; + os_log_error(_log, "Connection cleanup completed, waiting for reconnection"); } @end + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncSocketLineProcessor.m b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncSocketLineProcessor.m index 818666db51315..22f2eecfa2be7 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncSocketLineProcessor.m +++ b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncSocketLineProcessor.m @@ -4,90 +4,119 @@ */ #import +#import #import "FinderSyncSocketLineProcessor.h" +static os_log_t getFinderSyncSocketLineProcessorLogger(void) { + static dispatch_once_t onceToken; + static os_log_t logger = NULL; + dispatch_once(&onceToken, ^{ + NSBundle *bundle = [NSBundle bundleForClass:[FinderSyncSocketLineProcessor class]]; + NSString *subsystem = bundle.bundleIdentifier ?: @"FinderSyncExt"; + logger = os_log_create(subsystem.UTF8String, "FinderSyncSocketLineProcessor"); + }); + return logger; +} + +@interface FinderSyncSocketLineProcessor() +{ + os_log_t _log; +} +@end + @implementation FinderSyncSocketLineProcessor -(instancetype)initWithDelegate:(id)delegate { - NSLog(@"Init line processor with delegate."); + os_log_t logger = getFinderSyncSocketLineProcessorLogger(); + os_log_debug(logger, "Initializing FinderSyncSocketLineProcessor with delegate"); self = [super init]; if (self) { + _log = logger; self.delegate = delegate; + os_log_debug(logger, "FinderSyncSocketLineProcessor initialization completed"); } return self; } -(void)process:(NSString*)line { - NSLog(@"Processing line: '%@'", line); + os_log_debug(_log, "Processing line: %{public}@", line); NSArray *split = [line componentsSeparatedByString:@":"]; NSString *command = [split objectAtIndex:0]; - NSLog(@"Command: %@", command); + os_log_debug(_log, "Command: %{public}@", command); if([command isEqualToString:@"STATUS"]) { NSString *result = [split objectAtIndex:1]; NSArray *pathSplit = [split subarrayWithRange:NSMakeRange(2, [split count] - 2)]; // Get everything after location 2 NSString *path = [pathSplit componentsJoinedByString:@":"]; + os_log_debug(_log, "STATUS command: result=%{public}@, path=%{public}@", result, path); dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"Setting result %@ for path %@", result, path); + os_log_debug(_log, "Setting result %{public}@ for path %{public}@", result, path); [self.delegate setResult:result forPath:path]; }); } else if([command isEqualToString:@"UPDATE_VIEW"]) { NSString *path = [split objectAtIndex:1]; + os_log_debug(_log, "UPDATE_VIEW command: path=%{public}@", path); dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"Re-fetching filename cache for path %@", path); + os_log_debug(_log, "Re-fetching filename cache for path %{public}@", path); [self.delegate reFetchFileNameCacheForPath:path]; }); } else if([command isEqualToString:@"REGISTER_PATH"]) { NSString *path = [split objectAtIndex:1]; + os_log_debug(_log, "REGISTER_PATH command: path=%{public}@", path); dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"Registering path %@", path); + os_log_debug(_log, "Registering path %{public}@", path); [self.delegate registerPath:path]; }); } else if([command isEqualToString:@"UNREGISTER_PATH"]) { NSString *path = [split objectAtIndex:1]; + os_log_debug(_log, "UNREGISTER_PATH command: path=%{public}@", path); dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"Unregistering path %@", path); + os_log_debug(_log, "Unregistering path %{public}@", path); [self.delegate unregisterPath:path]; }); } else if([command isEqualToString:@"GET_STRINGS"]) { + os_log_debug(_log, "GET_STRINGS command: %{public}@", [split objectAtIndex:1] ?: @"(no subcommand)"); // BEGIN and END messages, do nothing. return; } else if([command isEqualToString:@"STRING"]) { NSString *key = [split objectAtIndex:1]; NSString *value = [split objectAtIndex:2]; + os_log_debug(_log, "STRING command: key=%{public}@, value=%{public}@", key, value); dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"Setting string %@ to value %@", key, value); + os_log_debug(_log, "Setting string %{public}@ to value %{public}@", key, value); [self.delegate setString:key value:value]; }); } else if([command isEqualToString:@"GET_MENU_ITEMS"]) { + os_log_debug(_log, "GET_MENU_ITEMS command: subcommand=%{public}@", [split objectAtIndex:1] ?: @"(no subcommand)"); if([[split objectAtIndex:1] isEqualToString:@"BEGIN"]) { dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"Resetting menu items."); + os_log_debug(_log, "Resetting menu items"); [self.delegate resetMenuItems]; }); } else { - NSLog(@"Emitting menu has completed signal."); + os_log_debug(_log, "Emitting menu has completed signal"); [self.delegate menuHasCompleted]; } } else if([command isEqualToString:@"MENU_ITEM"]) { NSDictionary *item = @{@"command": [split objectAtIndex:1], @"flags": [split objectAtIndex:2], @"text": [split objectAtIndex:3]}; + os_log_debug(_log, "MENU_ITEM command: command=%{public}@, flags=%{public}@, text=%{public}@", [split objectAtIndex:1], [split objectAtIndex:2], [split objectAtIndex:3]); dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"Adding menu item with command %@, flags %@, and text %@", [split objectAtIndex:1], [split objectAtIndex:2], [split objectAtIndex:3]); + os_log_debug(_log, "Adding menu item with command %{public}@, flags %{public}@, and text %{public}@", [split objectAtIndex:1], [split objectAtIndex:2], [split objectAtIndex:3]); [self.delegate addMenuItem:item]; }); } else { - // LOG UNKNOWN COMMAND - NSLog(@"Unknown command: %@", command); + os_log_error(_log, "Unknown command: %{public}@", command); } + os_log_debug(_log, "Line processing completed"); } @end diff --git a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Info.plist b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Info.plist index 927085601d11d..75f88b0ba1536 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Info.plist +++ b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Info.plist @@ -2,6 +2,8 @@ + NCApplicationGroupIdentifier + $(DEVELOPMENT_TEAM).$(OC_APPLICATION_REV_DOMAIN) NSAppTransportSecurity NSAllowsArbitraryLoads