diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt-Bridging-Header.h b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt-Bridging-Header.h new file mode 100644 index 0000000000000..4bbab0d379bcd --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt-Bridging-Header.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#ifndef FileProviderExt_Bridging_Header_h +#define FileProviderExt_Bridging_Header_h + +#import "Services/ClientCommunicationProtocol.h" + +#endif /* FileProviderExt_Bridging_Header_h */ diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift index 26c42aca87be0..a357b79eae20a 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift @@ -18,8 +18,38 @@ import NCDesktopClientSocketKit import NextcloudKit import OSLog -extension FileProviderExtension { - func sendFileProviderDomainIdentifier() { +extension FileProviderExtension: NSFileProviderServicing { + /* + This FileProviderExtension extension contains everything needed to communicate with the client. + We have two systems for communicating between the extensions and the client. + + Apple's XPC based File Provider APIs let us easily communicate client -> extension. + This is what ClientCommunicationService is for. + + We also use sockets, because the File Provider XPC system does not let us easily talk from + extension->client. + We need this because the extension needs to be able to request account details. We can't + reliably do this via XPC because the extensions get torn down by the system, out of the control + of the app, and we can receive nil/no services from NSFileProviderManager. Once this is done + then XPC works ok. + */ + func supportedServiceSources( + for itemIdentifier: NSFileProviderItemIdentifier, + completionHandler: @escaping ([NSFileProviderServiceSource]?, Error?) -> Void + ) -> Progress { + Logger.desktopClientConnection.debug("Serving supported service sources") + let clientCommService = ClientCommunicationService(fpExtension: self) + let services = [clientCommService] + completionHandler(services, nil) + let progress = Progress() + progress.cancellationHandler = { + let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError) + completionHandler(nil, error) + } + return progress + } + + @objc func sendFileProviderDomainIdentifier() { let command = "FILE_PROVIDER_DOMAIN_IDENTIFIER_REQUEST_REPLY" let argument = domain.identifier.rawValue let message = command + ":" + argument + "\n" @@ -57,8 +87,10 @@ extension FileProviderExtension { } } - func setupDomainAccount(user: String, serverUrl: String, password: String) { - ncAccount = NextcloudAccount(user: user, serverUrl: serverUrl, password: password) + @objc func setupDomainAccount(user: String, serverUrl: String, password: String) { + let newNcAccount = NextcloudAccount(user: user, serverUrl: serverUrl, password: password) + guard newNcAccount != ncAccount else { return } + ncAccount = newNcAccount ncKit.setup( user: ncAccount!.username, userId: ncAccount!.username, @@ -66,7 +98,7 @@ extension FileProviderExtension { urlBase: ncAccount!.serverUrl, userAgent: "Nextcloud-macOS/FileProviderExt", nextcloudVersion: 25, - delegate: nil) // TODO: add delegate methods for self + delegate: nil) // TODO: add delegate methods for self Logger.fileProviderExtension.info( "Nextcloud account set up in File Provider extension for user: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)" @@ -75,7 +107,7 @@ extension FileProviderExtension { signalEnumeratorAfterAccountSetup() } - func removeAccountConfig() { + @objc func removeAccountConfig() { Logger.fileProviderExtension.info( "Received instruction to remove account data for user \(self.ncAccount!.username, privacy: .public) at server \(self.ncAccount!.serverUrl, privacy: .public)" ) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift index 2f6039a71d816..3bf1613420da5 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift @@ -17,7 +17,7 @@ import NCDesktopClientSocketKit import NextcloudKit import OSLog -class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKCommonDelegate { +@objc class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKCommonDelegate { let domain: NSFileProviderDomain let ncKit = NextcloudKit() let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/NextcloudAccount.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/NextcloudAccount.swift index 9e5b64656608d..cb3b7ea628f9c 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/NextcloudAccount.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/NextcloudAccount.swift @@ -15,7 +15,7 @@ import FileProvider import Foundation -class NextcloudAccount: NSObject { +struct NextcloudAccount: Equatable { static let webDavFilesUrlSuffix: String = "/remote.php/dav/files/" let username, password, ncKitAccount, serverUrl, davFilesUrl: String @@ -25,7 +25,5 @@ class NextcloudAccount: NSObject { ncKitAccount = user + " " + serverUrl self.serverUrl = serverUrl davFilesUrl = serverUrl + NextcloudAccount.webDavFilesUrlSuffix + user - - super.init() } } diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h new file mode 100644 index 0000000000000..766eab1f6778d --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#ifndef ClientCommunicationProtocol_h +#define ClientCommunicationProtocol_h + +#import + +@protocol ClientCommunicationProtocol + +- (void)getExtensionAccountIdWithCompletionHandler:(void(^)(NSString *extensionAccountId, NSError *error))completionHandler; +- (void)configureAccountWithUser:(NSString *)user + serverUrl:(NSString *)serverUrl + password:(NSString *)password; +- (void)removeAccountConfig; + +@end + +#endif /* ClientCommunicationProtocol_h */ diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift new file mode 100644 index 0000000000000..83d26ae3a84ce --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +import Foundation +import FileProvider +import OSLog + +class ClientCommunicationService: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, ClientCommunicationProtocol { + let listener = NSXPCListener.anonymous() + let serviceName = NSFileProviderServiceName("com.nextcloud.desktopclient.ClientCommunicationService") + let fpExtension: FileProviderExtension + + init(fpExtension: FileProviderExtension) { + Logger.desktopClientConnection.debug("Instantiating client communication service") + self.fpExtension = fpExtension + super.init() + } + + func makeListenerEndpoint() throws -> NSXPCListenerEndpoint { + listener.delegate = self + listener.resume() + return listener.endpoint + } + + func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { + newConnection.exportedInterface = NSXPCInterface(with: ClientCommunicationProtocol.self) + newConnection.exportedObject = self + newConnection.resume() + return true + } + + //MARK: - Client Communication Protocol methods + + func getExtensionAccountId(completionHandler: @escaping (String?, Error?) -> Void) { + let accountUserId = self.fpExtension.domain.identifier.rawValue + Logger.desktopClientConnection.info("Sending extension account ID \(accountUserId, privacy: .public)") + completionHandler(accountUserId, nil) + } + + func configureAccount(withUser user: String, + serverUrl: String, + password: String) { + Logger.desktopClientConnection.info("Received configure account information over client communication service") + self.fpExtension.setupDomainAccount(user: user, + serverUrl: serverUrl, + password: password) + } + + func removeAccountConfig() { + self.fpExtension.removeAccountConfig() + } +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj index c592022afe4d6..e93e157378600 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj +++ b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -15,6 +15,7 @@ 5318AD9529BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */; }; 5318AD9729BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */; }; 5318AD9929BF58D000CBB71C /* NKError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */; }; + 5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */; }; 5352B36629DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */; }; 5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */; }; 5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */; }; @@ -145,6 +146,9 @@ 5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudLocalFileMetadataTable.swift; sourceTree = ""; }; 5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderMaterialisedEnumerationObserver.swift; sourceTree = ""; }; 5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKError+Extensions.swift"; sourceTree = ""; }; + 5350E4E72B0C514400F276CB /* ClientCommunicationProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ClientCommunicationProtocol.h; sourceTree = ""; }; + 5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientCommunicationService.swift; sourceTree = ""; }; + 5350E4EA2B0C9CE100F276CB /* FileProviderExt-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FileProviderExt-Bridging-Header.h"; sourceTree = ""; }; 5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudFilesDatabaseManager+Directories.swift"; sourceTree = ""; }; 5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudFilesDatabaseManager+LocalFiles.swift"; sourceTree = ""; }; 5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+Thumbnailing.swift"; sourceTree = ""; }; @@ -238,6 +242,15 @@ path = Database; sourceTree = ""; }; + 5350E4C72B0C368B00F276CB /* Services */ = { + isa = PBXGroup; + children = ( + 5350E4E72B0C514400F276CB /* ClientCommunicationProtocol.h */, + 5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */, + ); + path = Services; + sourceTree = ""; + }; 5352E85929B7BFB4002CE85C /* Extensions */ = { isa = PBXGroup; children = ( @@ -259,6 +272,7 @@ 538E396B27F4765000FA63D5 /* FileProviderExt */ = { isa = PBXGroup; children = ( + 5350E4C72B0C368B00F276CB /* Services */, 5318AD8F29BF406500CBB71C /* Database */, 5352E85929B7BFB4002CE85C /* Extensions */, 538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */, @@ -273,6 +287,7 @@ 536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */, 538E397327F4765000FA63D5 /* FileProviderExt.entitlements */, 538E397227F4765000FA63D5 /* Info.plist */, + 5350E4EA2B0C9CE100F276CB /* FileProviderExt-Bridging-Header.h */, ); path = FileProviderExt; sourceTree = ""; @@ -591,6 +606,7 @@ 538E396F27F4765000FA63D5 /* FileProviderItem.swift in Sources */, 5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */, 5318AD9129BF42FB00CBB71C /* NextcloudItemMetadataTable.swift in Sources */, + 5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */, 5352B36629DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift in Sources */, 5318AD9729BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift in Sources */, 5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */, @@ -705,6 +721,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "FileProviderExt/FileProviderExt-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -755,6 +772,7 @@ SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "FileProviderExt/FileProviderExt-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; }; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 1968683e23415..4a6e4d27fb1b1 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -286,6 +286,9 @@ IF( APPLE ) if (BUILD_FILE_PROVIDER_MODULE) list(APPEND client_SRCS + # Symlinks to files in shell_integration/MacOSX/NextcloudIntegration/ + macOS/ClientCommunicationProtocol.h + # End of symlink files macOS/fileprovider.h macOS/fileprovider_mac.mm macOS/fileproviderdomainmanager.h @@ -294,7 +297,11 @@ IF( APPLE ) macOS/fileprovidersocketcontroller.cpp macOS/fileprovidersocketserver.h macOS/fileprovidersocketserver.cpp - macOS/fileprovidersocketserver_mac.mm) + macOS/fileprovidersocketserver_mac.mm + macOS/fileproviderxpc.h + macOS/fileproviderxpc_mac.mm + macOS/fileproviderxpc_mac_utils.h + macOS/fileproviderxpc_mac_utils.mm) endif() if(SPARKLE_FOUND AND BUILD_UPDATER) diff --git a/src/gui/application.cpp b/src/gui/application.cpp index c318868907887..ab80245bb24e6 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -349,10 +349,6 @@ Application::Application(int &argc, char **argv) connect(this, &SharedTools::QtSingleApplication::messageReceived, this, &Application::slotParseMessage); -#if defined(BUILD_FILE_PROVIDER_MODULE) - _fileProvider.reset(new Mac::FileProvider); -#endif - // create accounts and folders from a legacy desktop client or from the current config file setupAccountsAndFolders(); @@ -420,6 +416,10 @@ Application::Application(int &argc, char **argv) AccountSetupCommandLineManager::instance()->setupAccountFromCommandLine(); } AccountSetupCommandLineManager::destroy(); + +#if defined(BUILD_FILE_PROVIDER_MODULE) + _fileProvider.reset(new Mac::FileProvider); +#endif } Application::~Application() diff --git a/src/gui/macOS/ClientCommunicationProtocol.h b/src/gui/macOS/ClientCommunicationProtocol.h new file mode 120000 index 0000000000000..bc406ebc7f8bf --- /dev/null +++ b/src/gui/macOS/ClientCommunicationProtocol.h @@ -0,0 +1 @@ +../../../shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationProtocol.h \ No newline at end of file diff --git a/src/gui/macOS/fileprovider.h b/src/gui/macOS/fileprovider.h index d45ba987d2a5b..e9d89b7a5ba8a 100644 --- a/src/gui/macOS/fileprovider.h +++ b/src/gui/macOS/fileprovider.h @@ -18,6 +18,7 @@ #include "fileproviderdomainmanager.h" #include "fileprovidersocketserver.h" +#include "fileproviderxpc.h" namespace OCC { @@ -38,9 +39,13 @@ class FileProvider : public QObject static bool fileProviderAvailable(); +private slots: + void configureXPC(); + private: std::unique_ptr _domainManager; std::unique_ptr _socketServer; + std::unique_ptr _xpc; static FileProvider *_instance; explicit FileProvider(QObject * const parent = nullptr); diff --git a/src/gui/macOS/fileprovider_mac.mm b/src/gui/macOS/fileprovider_mac.mm index 56db4d2447b07..f45b915569467 100644 --- a/src/gui/macOS/fileprovider_mac.mm +++ b/src/gui/macOS/fileprovider_mac.mm @@ -44,14 +44,16 @@ } qCInfo(lcMacFileProvider) << "Initialising file provider domain manager."; - _domainManager = std::make_unique(new FileProviderDomainManager(this)); + _domainManager = std::make_unique(this); if (_domainManager) { + connect(_domainManager.get(), &FileProviderDomainManager::domainSetupComplete, this, &FileProvider::configureXPC); + _domainManager->start(); qCDebug(lcMacFileProvider()) << "Initialized file provider domain manager"; } qCDebug(lcMacFileProvider) << "Initialising file provider socket server."; - _socketServer = std::make_unique(new FileProviderSocketServer(this)); + _socketServer = std::make_unique(this); if (_socketServer) { qCDebug(lcMacFileProvider) << "Initialised file provider socket server."; @@ -88,5 +90,17 @@ return false; } +void FileProvider::configureXPC() +{ + _xpc = std::make_unique(new FileProviderXPC(this)); + if (_xpc) { + qCInfo(lcMacFileProvider) << "Initialised file provider XPC."; + _xpc->connectToExtensions(); + _xpc->configureExtensions(); + } else { + qCWarning(lcMacFileProvider) << "Could not initialise file provider XPC."; + } +} + } // namespace Mac } // namespace OCC diff --git a/src/gui/macOS/fileproviderdomainmanager.h b/src/gui/macOS/fileproviderdomainmanager.h index 3aa80d39be3c5..33347dc5e1a30 100644 --- a/src/gui/macOS/fileproviderdomainmanager.h +++ b/src/gui/macOS/fileproviderdomainmanager.h @@ -33,6 +33,10 @@ class FileProviderDomainManager : public QObject ~FileProviderDomainManager() override; static AccountStatePtr accountStateFromFileProviderDomainIdentifier(const QString &domainIdentifier); + void start(); + +signals: + void domainSetupComplete(); private slots: void setupFileProviderDomains(); diff --git a/src/gui/macOS/fileproviderdomainmanager_mac.mm b/src/gui/macOS/fileproviderdomainmanager_mac.mm index 29b3ee244c27b..d6c9acda6dc4b 100644 --- a/src/gui/macOS/fileproviderdomainmanager_mac.mm +++ b/src/gui/macOS/fileproviderdomainmanager_mac.mm @@ -82,9 +82,9 @@ bool accountFilesPushNotificationsReady(const OCC::AccountPtr &account) namespace Mac { -class API_AVAILABLE(macos(11.0)) FileProviderDomainManager::MacImplementation { - - public: +class API_AVAILABLE(macos(11.0)) FileProviderDomainManager::MacImplementation +{ +public: MacImplementation() = default; ~MacImplementation() = default; @@ -96,16 +96,16 @@ void findExistingFileProviderDomains() dispatch_group_enter(dispatchGroup); [NSFileProviderManager getDomainsWithCompletionHandler:^(NSArray * const domains, NSError * const error) { - if(error) { - qCDebug(lcMacFileProviderDomainManager) << "Could not get existing file provider domains: " - << error.code - << error.localizedDescription; + if (error) { + qCWarning(lcMacFileProviderDomainManager) << "Could not get existing file provider domains: " + << error.code + << error.localizedDescription; dispatch_group_leave(dispatchGroup); return; } if (domains.count == 0) { - qCDebug(lcMacFileProviderDomainManager) << "Found no existing file provider domains"; + qCInfo(lcMacFileProviderDomainManager) << "Found no existing file provider domains"; dispatch_group_leave(dispatchGroup); return; } @@ -118,33 +118,36 @@ void findExistingFileProviderDomains() accountState->account() && domainDisplayNameForAccount(accountState->account()) == QString::fromNSString(domain.displayName)) { - qCDebug(lcMacFileProviderDomainManager) << "Found existing file provider domain for account:" - << accountState->account()->displayName(); + qCInfo(lcMacFileProviderDomainManager) << "Found existing file provider domain for account:" + << accountState->account()->displayName(); [domain retain]; _registeredDomains.insert(accountId, domain); NSFileProviderManager * const fpManager = [NSFileProviderManager managerForDomain:domain]; [fpManager reconnectWithCompletionHandler:^(NSError * const error) { if (error) { - qCDebug(lcMacFileProviderDomainManager) << "Error reconnecting file provider domain: " - << domain.displayName - << error.code - << error.localizedDescription; + qCWarning(lcMacFileProviderDomainManager) << "Error reconnecting file provider domain: " + << domain.displayName + << error.code + << error.localizedDescription; return; } - qCDebug(lcMacFileProviderDomainManager) << "Successfully reconnected file provider domain: " + qCInfo(lcMacFileProviderDomainManager) << "Successfully reconnected file provider domain: " << domain.displayName; }]; } else { - qCDebug(lcMacFileProviderDomainManager) << "Found existing file provider domain with no known configured account:" - << domain.displayName; + qCInfo(lcMacFileProviderDomainManager) << "Found existing file provider domain with no known configured account:" + << domain.displayName + << accountState + << (accountState ? "NON-NULL ACCOUNTSTATE" : "NULL") + << (accountState && accountState->account() ? domainDisplayNameForAccount(accountState->account()) : "NULL"); [NSFileProviderManager removeDomain:domain completionHandler:^(NSError * const error) { - if(error) { - qCDebug(lcMacFileProviderDomainManager) << "Error removing file provider domain: " - << error.code - << error.localizedDescription; + if (error) { + qCWarning(lcMacFileProviderDomainManager) << "Error removing file provider domain: " + << error.code + << error.localizedDescription; } }]; } @@ -167,10 +170,12 @@ void addFileProviderDomain(const AccountState * const accountState) const auto domainDisplayName = domainDisplayNameForAccount(account); const auto domainId = domainIdentifierForAccount(account); - qCDebug(lcMacFileProviderDomainManager) << "Adding new file provider domain with id: " << domainId; + qCInfo(lcMacFileProviderDomainManager) << "Adding new file provider domain with id: " + << domainId; - if(_registeredDomains.contains(domainId) && _registeredDomains.value(domainId) != nil) { - qCDebug(lcMacFileProviderDomainManager) << "File provider domain with id already exists: " << domainId; + if (_registeredDomains.contains(domainId) && _registeredDomains.value(domainId) != nil) { + qCDebug(lcMacFileProviderDomainManager) << "File provider domain with id already exists: " + << domainId; return; } @@ -180,9 +185,9 @@ void addFileProviderDomain(const AccountState * const accountState) [NSFileProviderManager addDomain:fileProviderDomain completionHandler:^(NSError * const error) { if(error) { - qCDebug(lcMacFileProviderDomainManager) << "Error adding file provider domain: " - << error.code - << error.localizedDescription; + qCWarning(lcMacFileProviderDomainManager) << "Error adding file provider domain: " + << error.code + << error.localizedDescription; } _registeredDomains.insert(domainId, fileProviderDomain); @@ -198,20 +203,22 @@ void removeFileProviderDomain(const AccountState * const accountState) Q_ASSERT(account); const auto domainId = domainIdentifierForAccount(account); - qCDebug(lcMacFileProviderDomainManager) << "Removing file provider domain with id: " << domainId; + qCInfo(lcMacFileProviderDomainManager) << "Removing file provider domain with id: " + << domainId; - if(!_registeredDomains.contains(domainId)) { - qCDebug(lcMacFileProviderDomainManager) << "File provider domain not found for id: " << domainId; + if (!_registeredDomains.contains(domainId)) { + qCWarning(lcMacFileProviderDomainManager) << "File provider domain not found for id: " + << domainId; return; } NSFileProviderDomain * const fileProviderDomain = _registeredDomains[domainId]; [NSFileProviderManager removeDomain:fileProviderDomain completionHandler:^(NSError *error) { - if(error) { - qCDebug(lcMacFileProviderDomainManager) << "Error removing file provider domain: " - << error.code - << error.localizedDescription; + if (error) { + qCWarning(lcMacFileProviderDomainManager) << "Error removing file provider domain: " + << error.code + << error.localizedDescription; } NSFileProviderDomain * const domain = _registeredDomains.take(domainId); @@ -247,13 +254,13 @@ void removeAllFileProviderDomains() void wipeAllFileProviderDomains() { if (@available(macOS 12.0, *)) { - qCDebug(lcMacFileProviderDomainManager) << "Removing and wiping all file provider domains"; + qCInfo(lcMacFileProviderDomainManager) << "Removing and wiping all file provider domains"; [NSFileProviderManager getDomainsWithCompletionHandler:^(NSArray * const domains, NSError * const error) { if (error) { - qCDebug(lcMacFileProviderDomainManager) << "Error removing and wiping file provider domains: " - << error.code - << error.localizedDescription; + qCWarning(lcMacFileProviderDomainManager) << "Error removing and wiping file provider domains: " + << error.code + << error.localizedDescription; return; } @@ -262,10 +269,10 @@ void wipeAllFileProviderDomains() Q_UNUSED(preservedLocation) if (error) { - qCDebug(lcMacFileProviderDomainManager) << "Error removing and wiping file provider domain: " - << domain.displayName - << error.code - << error.localizedDescription; + qCWarning(lcMacFileProviderDomainManager) << "Error removing and wiping file provider domain: " + << domain.displayName + << error.code + << error.localizedDescription; return; } @@ -277,7 +284,7 @@ void wipeAllFileProviderDomains() } }]; } else if (@available(macOS 11.0, *)) { - qCDebug(lcMacFileProviderDomainManager) << "Removing all file provider domains, can't specify wipe on macOS 11"; + qCInfo(lcMacFileProviderDomainManager) << "Removing all file provider domains, can't specify wipe on macOS 11"; removeAllFileProviderDomains(); } } @@ -290,10 +297,12 @@ void disconnectFileProviderDomainForAccount(const AccountState * const accountSt Q_ASSERT(account); const auto domainId = domainIdentifierForAccount(account); - qCDebug(lcMacFileProviderDomainManager) << "Disconnecting file provider domain with id: " << domainId; + qCInfo(lcMacFileProviderDomainManager) << "Disconnecting file provider domain with id: " + << domainId; if(!_registeredDomains.contains(domainId)) { - qCDebug(lcMacFileProviderDomainManager) << "File provider domain not found for id: " << domainId; + qCInfo(lcMacFileProviderDomainManager) << "File provider domain not found for id: " + << domainId; return; } @@ -305,15 +314,15 @@ void disconnectFileProviderDomainForAccount(const AccountState * const accountSt options:NSFileProviderManagerDisconnectionOptionsTemporary completionHandler:^(NSError * const error) { if (error) { - qCDebug(lcMacFileProviderDomainManager) << "Error disconnecting file provider domain: " - << fileProviderDomain.displayName - << error.code - << error.localizedDescription; + qCWarning(lcMacFileProviderDomainManager) << "Error disconnecting file provider domain: " + << fileProviderDomain.displayName + << error.code + << error.localizedDescription; return; } - qCDebug(lcMacFileProviderDomainManager) << "Successfully disconnected file provider domain: " - << fileProviderDomain.displayName; + qCInfo(lcMacFileProviderDomainManager) << "Successfully disconnected file provider domain: " + << fileProviderDomain.displayName; }]; } } @@ -326,10 +335,12 @@ void reconnectFileProviderDomainForAccount(const AccountState * const accountSta Q_ASSERT(account); const auto domainId = domainIdentifierForAccount(account); - qCDebug(lcMacFileProviderDomainManager) << "Reconnecting file provider domain with id: " << domainId; + qCInfo(lcMacFileProviderDomainManager) << "Reconnecting file provider domain with id: " + << domainId; if(!_registeredDomains.contains(domainId)) { - qCDebug(lcMacFileProviderDomainManager) << "File provider domain not found for id: " << domainId; + qCInfo(lcMacFileProviderDomainManager) << "File provider domain not found for id: " + << domainId; return; } @@ -339,15 +350,15 @@ void reconnectFileProviderDomainForAccount(const AccountState * const accountSta NSFileProviderManager * const fpManager = [NSFileProviderManager managerForDomain:fileProviderDomain]; [fpManager reconnectWithCompletionHandler:^(NSError * const error) { if (error) { - qCDebug(lcMacFileProviderDomainManager) << "Error reconnecting file provider domain: " - << fileProviderDomain.displayName - << error.code - << error.localizedDescription; + qCWarning(lcMacFileProviderDomainManager) << "Error reconnecting file provider domain: " + << fileProviderDomain.displayName + << error.code + << error.localizedDescription; return; } - qCDebug(lcMacFileProviderDomainManager) << "Successfully reconnected file provider domain: " - << fileProviderDomain.displayName; + qCInfo(lcMacFileProviderDomainManager) << "Successfully reconnected file provider domain: " + << fileProviderDomain.displayName; signalEnumeratorChanged(account.get()); }]; @@ -360,10 +371,12 @@ void signalEnumeratorChanged(const Account * const account) Q_ASSERT(account); const auto domainId = domainIdentifierForAccount(account); - qCDebug(lcMacFileProviderDomainManager) << "Signalling enumerator changed in file provider domain for account with id: " << domainId; + qCInfo(lcMacFileProviderDomainManager) << "Signalling enumerator changed in file provider domain for account with id: " + << domainId; if(!_registeredDomains.contains(domainId)) { - qCDebug(lcMacFileProviderDomainManager) << "File provider domain not found for id: " << domainId; + qCInfo(lcMacFileProviderDomainManager) << "File provider domain not found for id: " + << domainId; return; } @@ -373,14 +386,15 @@ void signalEnumeratorChanged(const Account * const account) NSFileProviderManager * const fpManager = [NSFileProviderManager managerForDomain:fileProviderDomain]; [fpManager signalEnumeratorForContainerItemIdentifier:NSFileProviderWorkingSetContainerItemIdentifier completionHandler:^(NSError * const error) { if (error != nil) { - qCDebug(lcMacFileProviderDomainManager) << "Error signalling enumerator changed for working set:" - << error.localizedDescription; + qCWarning(lcMacFileProviderDomainManager) << "Error signalling enumerator changed for working set:" + << error.localizedDescription; } }]; } } - QStringList configuredDomainIds() const { + QStringList configuredDomainIds() const + { return _registeredDomains.keys(); } @@ -393,29 +407,6 @@ QStringList configuredDomainIds() const { { if (@available(macOS 11.0, *)) { d.reset(new FileProviderDomainManager::MacImplementation()); - - ConfigFile cfg; - std::chrono::milliseconds polltime = cfg.remotePollInterval(); - _enumeratorSignallingTimer.setInterval(polltime.count()); - connect(&_enumeratorSignallingTimer, &QTimer::timeout, - this, &FileProviderDomainManager::slotEnumeratorSignallingTimerTimeout); - _enumeratorSignallingTimer.start(); - - setupFileProviderDomains(); - - connect(AccountManager::instance(), &AccountManager::accountAdded, - this, &FileProviderDomainManager::addFileProviderDomainForAccount); - // If an account is deleted from the client, accountSyncConnectionRemoved will be - // emitted first. So we treat accountRemoved as only being relevant to client - // shutdowns. - connect(AccountManager::instance(), &AccountManager::accountSyncConnectionRemoved, - this, &FileProviderDomainManager::removeFileProviderDomainForAccount); - connect(AccountManager::instance(), &AccountManager::accountRemoved, - this, [this](const AccountState * const accountState) { - - const auto trReason = tr("%1 application has been closed. Reopen to reconnect.").arg(APPLICATION_NAME); - disconnectFileProviderDomainForAccount(accountState, trReason); - }); } else { qCWarning(lcMacFileProviderDomainManager()) << "Trying to run File Provider on system that does not support it."; } @@ -423,6 +414,31 @@ QStringList configuredDomainIds() const { FileProviderDomainManager::~FileProviderDomainManager() = default; +void FileProviderDomainManager::start() +{ + ConfigFile cfg; + std::chrono::milliseconds polltime = cfg.remotePollInterval(); + _enumeratorSignallingTimer.setInterval(polltime.count()); + connect(&_enumeratorSignallingTimer, &QTimer::timeout, + this, &FileProviderDomainManager::slotEnumeratorSignallingTimerTimeout); + _enumeratorSignallingTimer.start(); + + setupFileProviderDomains(); + + connect(AccountManager::instance(), &AccountManager::accountAdded, + this, &FileProviderDomainManager::addFileProviderDomainForAccount); + // If an account is deleted from the client, accountSyncConnectionRemoved will be + // emitted first. So we treat accountRemoved as only being relevant to client + // shutdowns. + connect(AccountManager::instance(), &AccountManager::accountSyncConnectionRemoved, + this, &FileProviderDomainManager::removeFileProviderDomainForAccount); + connect(AccountManager::instance(), &AccountManager::accountRemoved, + this, [this](const AccountState * const accountState) { + const auto trReason = tr("%1 application has been closed. Reopen to reconnect.").arg(APPLICATION_NAME); + disconnectFileProviderDomainForAccount(accountState, trReason); + }); +} + void FileProviderDomainManager::setupFileProviderDomains() { if (!d) { @@ -434,6 +450,8 @@ QStringList configuredDomainIds() const { for(auto &accountState : AccountManager::instance()->accounts()) { addFileProviderDomainForAccount(accountState.data()); } + + emit domainSetupComplete(); } void FileProviderDomainManager::addFileProviderDomainForAccount(const AccountState * const accountState) diff --git a/src/gui/macOS/fileprovidersocketcontroller.cpp b/src/gui/macOS/fileprovidersocketcontroller.cpp index 4790123e35de7..032ac22275031 100644 --- a/src/gui/macOS/fileprovidersocketcontroller.cpp +++ b/src/gui/macOS/fileprovidersocketcontroller.cpp @@ -101,7 +101,7 @@ void FileProviderSocketController::sendMessage(const QString &message) const return; } - qCDebug(lcFileProviderSocketController) << "Sending File Provider socket message:" << message; + qCInfo(lcFileProviderSocketController) << "Sending File Provider socket message:" << message; const auto lineEndChar = '\n'; const auto messageToSend = message.endsWith(lineEndChar) ? message : message + lineEndChar; const auto bytesToSend = messageToSend.toUtf8(); @@ -112,7 +112,6 @@ void FileProviderSocketController::sendMessage(const QString &message) const } } - void FileProviderSocketController::start() { Q_ASSERT(_socket); @@ -121,6 +120,12 @@ void FileProviderSocketController::start() return; } + /* + * We have a new file provider extension connection. When this happens, we: + * 1. Request the file provider domain identifier + * 2. Receive the file provider domain identifier from the extension + * 3. Send the account details to the extension according to the domain identifier + */ requestFileProviderDomainInfo(); } @@ -178,14 +183,18 @@ void FileProviderSocketController::sendAccountDetails() const const auto account = _accountState->account(); Q_ASSERT(account); - qCDebug(lcFileProviderSocketController) << "About to send account details to file provider extension" - << account->displayName(); + qCInfo(lcFileProviderSocketController) << "About to send account details to file provider extension" + << account->displayName(); - connect(_accountState.data(), &AccountState::stateChanged, this, &FileProviderSocketController::slotAccountStateChanged, Qt::UniqueConnection); + // Even though we have XPC send over the account details and related calls when the account state changes, in the + // brief window where we start the file provider extension on app startup and the account state changes, we need to + // be able to send over the details when the account is done getting configured. + connect(_accountState.data(), &AccountState::stateChanged, + this, &FileProviderSocketController::slotAccountStateChanged, Qt::UniqueConnection); if (!_accountState->isConnected()) { - qCDebug(lcFileProviderSocketController) << "Not sending account details yet as account is not connected" - << account->displayName(); + qCWarning(lcFileProviderSocketController) << "Not sending account details yet as account is not connected" + << account->displayName(); return; } diff --git a/src/gui/macOS/fileprovidersocketserver.h b/src/gui/macOS/fileprovidersocketserver.h index 893de666e5a46..e650e0ca2b4ac 100644 --- a/src/gui/macOS/fileprovidersocketserver.h +++ b/src/gui/macOS/fileprovidersocketserver.h @@ -26,6 +26,19 @@ using FileProviderSocketControllerPtr = QPointer; QString fileProviderSocketPath(); +/* + * Establishes communication between the app and the file provider extension. + * This is done via a local socket server. + * Note that this should be used for extension->client communication. + * + * We can communicate bidirectionally, but the File Provider XPC API is a better interface for this as we cannot account + * for the lifetime of a file provider extension when using sockets, and cannot control this on the client side. + * Use FileProviderXPC for client->extension communication when possible. + * + * This socket system is critical for the file provider extensions to be able to request authentication details. + * + * TODO: This should rewritten to use XPC instead of sockets + */ class FileProviderSocketServer : public QObject { Q_OBJECT diff --git a/src/gui/macOS/fileproviderxpc.h b/src/gui/macOS/fileproviderxpc.h new file mode 100644 index 0000000000000..25efb05ad2894 --- /dev/null +++ b/src/gui/macOS/fileproviderxpc.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include +#include + +#include "accountstate.h" + +#pragma once + +namespace OCC::Mac { + +/* + * Establishes communication between the app and the file provider extension. + * This is done via File Provider's XPC services API. + * Note that this is for client->extension communication, not the other way around. + * This is because the extension does not have a way to communicate with the client through the File Provider XPC API + */ +class FileProviderXPC : public QObject +{ + Q_OBJECT + +public: + explicit FileProviderXPC(QObject *parent = nullptr); + +public slots: + void connectToExtensions(); + void configureExtensions(); + void authenticateExtension(const QString &extensionAccountId) const; + void unauthenticateExtension(const QString &extensionAccountId) const; + +private slots: + void slotAccountStateChanged(AccountState::State state) const; + +private: + QHash _clientCommServices; +}; + +} // namespace OCC::Mac diff --git a/src/gui/macOS/fileproviderxpc_mac.mm b/src/gui/macOS/fileproviderxpc_mac.mm new file mode 100644 index 0000000000000..399c1af6d4bf9 --- /dev/null +++ b/src/gui/macOS/fileproviderxpc_mac.mm @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "fileproviderxpc.h" + +#include + +#include "gui/accountmanager.h" +#include "gui/macOS/fileproviderdomainmanager.h" +#include "gui/macOS/fileproviderxpc_mac_utils.h" + +namespace OCC::Mac { + +Q_LOGGING_CATEGORY(lcFileProviderXPC, "nextcloud.gui.macos.fileprovider.xpc", QtInfoMsg) + +FileProviderXPC::FileProviderXPC(QObject *parent) + : QObject{parent} +{ +} + +void FileProviderXPC::connectToExtensions() +{ + qCInfo(lcFileProviderXPC) << "Starting file provider XPC"; + const auto managers = FileProviderXPCUtils::getDomainManagers(); + const auto fpServices = FileProviderXPCUtils::getFileProviderServices(managers); + const auto connections = FileProviderXPCUtils::connectToFileProviderServices(fpServices); + _clientCommServices = FileProviderXPCUtils::processClientCommunicationConnections(connections); +} + +void FileProviderXPC::configureExtensions() +{ + for (const auto &extensionNcAccount : _clientCommServices.keys()) { + qCInfo(lcFileProviderXPC) << "Sending message to client communication service"; + authenticateExtension(extensionNcAccount); + } +} + +void FileProviderXPC::authenticateExtension(const QString &extensionAccountId) const +{ + const auto accountState = FileProviderDomainManager::accountStateFromFileProviderDomainIdentifier(extensionAccountId); + if (!accountState) { + qCWarning(lcFileProviderXPC) << "Account state is null for received account" + << extensionAccountId; + return; + } + + connect(accountState.data(), &AccountState::stateChanged, this, &FileProviderXPC::slotAccountStateChanged, Qt::UniqueConnection); + + const auto account = accountState->account(); + const auto credentials = account->credentials(); + NSString *const user = credentials->user().toNSString(); + NSString *const serverUrl = account->url().toString().toNSString(); + NSString *const password = credentials->password().toNSString(); + + const auto clientCommService = (NSObject *)_clientCommServices.value(extensionAccountId); + [clientCommService configureAccountWithUser:user + serverUrl:serverUrl + password:password]; +} + +void FileProviderXPC::unauthenticateExtension(const QString &extensionAccountId) const +{ + qCInfo(lcFileProviderXPC) << "Unauthenticating extension" << extensionAccountId; + const auto clientCommService = (NSObject *)_clientCommServices.value(extensionAccountId); + [clientCommService removeAccountConfig]; +} + +void FileProviderXPC::slotAccountStateChanged(const AccountState::State state) const +{ + const auto slotSender = dynamic_cast(sender()); + Q_ASSERT(slotSender); + const auto extensionAccountId = slotSender->account()->userIdAtHostWithPort(); + + switch(state) { + case AccountState::Disconnected: + case AccountState::ConfigurationError: + case AccountState::NetworkError: + case AccountState::ServiceUnavailable: + case AccountState::MaintenanceMode: + // Do nothing, File Provider will by itself figure out connection issue + break; + case AccountState::SignedOut: + case AccountState::AskingCredentials: + case AccountState::RedirectDetected: + // Notify File Provider that it should show the not authenticated message + unauthenticateExtension(extensionAccountId); + break; + case AccountState::Connected: + // Provide credentials + authenticateExtension(extensionAccountId); + break; + } +} + +} // namespace OCC::Mac diff --git a/src/gui/macOS/fileproviderxpc_mac_utils.h b/src/gui/macOS/fileproviderxpc_mac_utils.h new file mode 100644 index 0000000000000..f66d585f6375a --- /dev/null +++ b/src/gui/macOS/fileproviderxpc_mac_utils.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include + +#import +#import + +#import "ClientCommunicationProtocol.h" + +namespace OCC::Mac::FileProviderXPCUtils { + +NSArray *getDomainManagers(); +NSArray *getDomainUrlsForManagers(NSArray *managers); +NSArray *> *getFileProviderServices(NSArray *managers); +NSArray *> *getFileProviderServicesAtUrls(NSArray *urls); +NSArray *connectToFileProviderServices(NSArray *> *fpServices); +void configureFileProviderConnection(NSXPCConnection *connection); +NSObject *getRemoteServiceObject(NSXPCConnection *connection, Protocol *protocol); +NSString *getExtensionAccountId(NSObject *clientCommService); +QHash processClientCommunicationConnections(NSArray *connections); + +} \ No newline at end of file diff --git a/src/gui/macOS/fileproviderxpc_mac_utils.mm b/src/gui/macOS/fileproviderxpc_mac_utils.mm new file mode 100644 index 0000000000000..eb385d1c42858 --- /dev/null +++ b/src/gui/macOS/fileproviderxpc_mac_utils.mm @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2023 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "fileproviderxpc_mac_utils.h" + +#include + +#include "gui/accountmanager.h" + +namespace { +const char *const clientCommunicationServiceName = "com.nextcloud.desktopclient.ClientCommunicationService"; +NSString *const nsClientCommunicationServiceName = [NSString stringWithUTF8String:clientCommunicationServiceName]; +} + +namespace OCC::Mac::FileProviderXPCUtils { + +Q_LOGGING_CATEGORY(lcFileProviderXPCUtils, "nextcloud.gui.macos.fileprovider.xpc.utils", QtInfoMsg) + +NSArray *getDomainManagers() +{ + dispatch_group_t group = dispatch_group_create(); + __block NSMutableArray *managers = NSMutableArray.array; + + dispatch_group_enter(group); + + // Set up connections for each domain + [NSFileProviderManager getDomainsWithCompletionHandler:^(NSArray *const domains, NSError *const error){ + if (error != nil) { + qCWarning(lcFileProviderXPCUtils) << "Error getting domains" << error; + dispatch_group_leave(group); + return; + } + + for (NSFileProviderDomain *const domain in domains) { + qCInfo(lcFileProviderXPCUtils) << "Got domain" << domain.identifier; + NSFileProviderManager *const manager = [NSFileProviderManager managerForDomain:domain]; + [manager retain]; + [managers addObject:manager]; + } + + dispatch_group_leave(group); + }]; + + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + + if (managers.count == 0) { + qCWarning(lcFileProviderXPCUtils) << "No domains found"; + } + + return managers.copy; +} + +// TODO: This should work for all service names, not just the communication service! +NSArray *> *getFileProviderServices(NSArray *managers) +{ + if (@available(macOS 13.0, *)) { + NSMutableArray *> *const fpServices = NSMutableArray.array; + dispatch_group_t group = dispatch_group_create(); + + for (NSFileProviderManager *const manager in managers) { + dispatch_group_enter(group); + [manager getServiceWithName:nsClientCommunicationServiceName + itemIdentifier:NSFileProviderRootContainerItemIdentifier + completionHandler:^(NSFileProviderService *const service, NSError *const error) { + if (error != nil) { + qCWarning(lcFileProviderXPCUtils) << "Error getting file provider service" << error; + } else if (service == nil) { + qCWarning(lcFileProviderXPCUtils) << "Service is nil"; + } else { + [service retain]; + [fpServices addObject:@{service.name: service}]; + } + dispatch_group_leave(group); + }]; + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + } + return fpServices.copy; + } else { + const auto domainUrls = FileProviderXPCUtils::getDomainUrlsForManagers(managers); + return FileProviderXPCUtils::getFileProviderServicesAtUrls(domainUrls); + } +} + +NSArray *getDomainUrlsForManagers(NSArray *managers) +{ + dispatch_group_t group = dispatch_group_create(); + __block NSMutableArray *urls = NSMutableArray.array; + + for (NSFileProviderManager *const manager in managers) { + + dispatch_group_enter(group); + + [manager getUserVisibleURLForItemIdentifier:NSFileProviderRootContainerItemIdentifier + completionHandler:^(NSURL *const url, NSError *const error){ + if (error != nil) { + qCWarning(lcFileProviderXPCUtils) << "Error getting user visible url" << error; + dispatch_group_leave(group); + return; + } + + qCDebug(lcFileProviderXPCUtils) << "Got user visible url" << url; + [urls addObject:url]; + dispatch_group_leave(group); + }]; + } + + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + + if (urls.count == 0) { + qCWarning(lcFileProviderXPCUtils) << "No urls found"; + } + + return urls.copy; +} + +NSArray *> *getFileProviderServicesAtUrls(NSArray *urls) +{ + dispatch_group_t group = dispatch_group_create(); + NSMutableArray *> *const fpServices = NSMutableArray.array; + + for (NSURL *const url in urls) { + dispatch_group_enter(group); + + [NSFileManager.defaultManager getFileProviderServicesForItemAtURL:url + completionHandler:^(NSDictionary *const services, NSError *const error){ + if (error != nil) { + qCWarning(lcFileProviderXPCUtils) << "Error getting file provider services" << error; + dispatch_group_leave(group); + return; + } + + qCInfo(lcFileProviderXPCUtils) << "Got file provider services for" + << url.absoluteString + << "has number of services:" + << services.count; + [fpServices addObject:services]; + dispatch_group_leave(group); + }]; + } + + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + + if (fpServices.count == 0) { + qCWarning(lcFileProviderXPCUtils) << "No file provider services found"; + } + + return fpServices.copy; +} + +NSArray *connectToFileProviderServices(NSArray *> *fpServices) +{ + dispatch_group_t group = dispatch_group_create(); + NSMutableArray *const connections = NSMutableArray.array; + + for (NSDictionary *const services in fpServices) { + NSArray *const serviceNamesArray = services.allKeys; + + for (NSFileProviderServiceName serviceName in serviceNamesArray) { + qCInfo(lcFileProviderXPCUtils) << "Got service" << serviceName; + + if (![serviceName isEqualToString:nsClientCommunicationServiceName]) { + continue; + } + + NSFileProviderService *const service = services[serviceName]; + dispatch_group_enter(group); + + [service getFileProviderConnectionWithCompletionHandler:^(NSXPCConnection *const connection, NSError *const error){ + if (error != nil) { + qCWarning(lcFileProviderXPCUtils) << "Error getting file provider connection" << error; + dispatch_group_leave(group); + return; + } + + qCInfo(lcFileProviderXPCUtils) << "Got file provider connection" << connection; + + if (connection == nil) { + qCWarning(lcFileProviderXPCUtils) << "Connection is nil"; + dispatch_group_leave(group); + return; + } + + [connection retain]; + [connections addObject:connection]; + dispatch_group_leave(group); + }]; + } + } + + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + return connections.copy; +} + +void configureFileProviderConnection(NSXPCConnection *const connection) +{ + Q_ASSERT(connection != nil); + connection.interruptionHandler = ^{ + qCInfo(lcFileProviderXPCUtils) << "File provider connection interrupted"; + }; + connection.invalidationHandler = ^{ + qCInfo(lcFileProviderXPCUtils) << "File provider connection invalidated"; + }; + [connection resume]; +} + +NSObject *getRemoteServiceObject(NSXPCConnection *const connection, Protocol *const protocol) +{ + Q_ASSERT(connection != nil); + Q_ASSERT(protocol != nil); + const id remoteServiceObject = [connection remoteObjectProxyWithErrorHandler:^(NSError *const error){ + qCWarning(lcFileProviderXPCUtils) << "Error getting remote object proxy" << error; + }]; + if (remoteServiceObject == nil) { + return nil; + } + if (![remoteServiceObject conformsToProtocol:@protocol(ClientCommunicationProtocol)]) { + qCWarning(lcFileProviderXPCUtils) << "Remote service object does not conform to protocol"; + return nil; + } + return remoteServiceObject; +} + +NSString *getExtensionAccountId(NSObject *const clientCommService) +{ + Q_ASSERT(clientCommService != nil); + __block NSString *extensionNcAccount; + dispatch_group_t group = dispatch_group_create(); + dispatch_group_enter(group); + [clientCommService getExtensionAccountIdWithCompletionHandler:^(NSString *const extensionAccountId, NSError *const error){ + if (error != nil) { + qCWarning(lcFileProviderXPCUtils) << "Error getting extension account id" << error; + dispatch_group_leave(group); + return; + } + extensionNcAccount = [NSString stringWithString:extensionAccountId]; + [extensionNcAccount retain]; + dispatch_group_leave(group); + }]; + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + return extensionNcAccount; +} + +QHash processClientCommunicationConnections(NSArray *const connections) +{ + QHash clientCommServices; + + for (NSXPCConnection * const connection in connections) { + const auto remoteObjectInterfaceProtocol = @protocol(ClientCommunicationProtocol); + connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:remoteObjectInterfaceProtocol]; + configureFileProviderConnection(connection); + + const auto clientCommService = (NSObject *)getRemoteServiceObject(connection, remoteObjectInterfaceProtocol); + if (clientCommService == nil) { + qCWarning(lcFileProviderXPCUtils) << "Client communication service is nil"; + continue; + } + [clientCommService retain]; + + const auto extensionNcAccount = getExtensionAccountId(clientCommService); + if (extensionNcAccount == nil) { + qCWarning(lcFileProviderXPCUtils) << "Extension account id is nil"; + continue; + } + qCInfo(lcFileProviderXPCUtils) << "Got extension account id" << extensionNcAccount.UTF8String; + clientCommServices.insert(QString::fromNSString(extensionNcAccount), clientCommService); + } + + return clientCommServices; +} + +} // namespace OCC::Mac::FileProviderXPCUtils