Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ IF( APPLE )
# End of XPC protocol files
macOS/fileprovider.h
macOS/fileprovider_mac.mm
macOS/fileproviderdomainidentifierpolicy.h
macOS/fileproviderdomainidentifierpolicy.cpp
macOS/fileproviderdomainmanager.h
macOS/fileproviderdomainmanager.mm
macOS/fileprovidereditlocallyjob.h
Expand Down
35 changes: 35 additions & 0 deletions src/gui/macOS/fileproviderdomainidentifierpolicy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "fileproviderdomainidentifierpolicy.h"

namespace OCC::Mac::FileProviderDomainIdentifierPolicy {

RegistrationDecision decideRegistration(const QString &storedIdentifier,
const QSet<QString> &registeredIdentifiers,
const bool domainListingSucceeded)
{
auto decision = RegistrationDecision{};

if (!domainListingSucceeded) {
decision.action = RegistrationAction::Abort;
return decision;
}

if (!storedIdentifier.isEmpty() && registeredIdentifiers.contains(storedIdentifier)) {
decision.action = RegistrationAction::Skip;
return decision;
}

if (!storedIdentifier.isEmpty()) {
decision.action = RegistrationAction::AddStored;
return decision;
}

decision.action = RegistrationAction::AddFresh;
return decision;
}

} // namespace OCC::Mac::FileProviderDomainIdentifierPolicy
48 changes: 48 additions & 0 deletions src/gui/macOS/fileproviderdomainidentifierpolicy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/

#pragma once

#include <QSet>
#include <QString>

namespace OCC::Mac::FileProviderDomainIdentifierPolicy {

/**
* @brief What `addFileProviderDomain` should do after listing the domains macOS currently has.
*/
enum class RegistrationAction {
Abort, //!< Domain listing failed; do not add or mint a new identifier.
Skip, //!< The stored identifier is already registered.
AddStored, //!< Re-register using the stored identifier (do not mint a new UUID).
AddFresh, //!< No stored identifier; mint a new UUID and register it.
};

/**
* @brief Result of `decideRegistration`.
*/
struct RegistrationDecision
{
RegistrationAction action = RegistrationAction::Abort;
};

/**
* @brief Decide how to register a file provider domain for an account.
*
* Minting a new UUID when the account already has an identifier is what produces a second
* Finder location (`… (date)`) after a listing failure or after the user removes the domain
* in System Settings. Re-adding with the same identifier is the File Provider contract for
* replacing a registration in place.
*
* @param storedIdentifier The account's persisted domain identifier, or empty if none.
* @param registeredIdentifiers Identifiers currently returned by `NSFileProviderManager`.
* @param domainListingSucceeded `false` when the system listing call reported an error; the
* registered set must then be treated as unknown, not empty.
*/
[[nodiscard]] RegistrationDecision decideRegistration(const QString &storedIdentifier,
const QSet<QString> &registeredIdentifiers,
bool domainListingSucceeded);

} // namespace OCC::Mac::FileProviderDomainIdentifierPolicy
8 changes: 6 additions & 2 deletions src/gui/macOS/fileproviderdomainmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ class FileProviderDomainManager : public QObject
~FileProviderDomainManager() override;

/**
* @brief Add a new file provider domain for the given account.
* @return The raw identifier of the added domain as a string.
* @brief Add a file provider domain for the given account, or return the existing one.
*
* Reuses the account's stored identifier when the domain is missing. Mints a new UUID
* only when the account has no identifier yet. Returns an empty string when domain
* listing fails, so a second Finder location is not created.
* @return The domain identifier, or an empty string on failure.
*/
QString addDomainForAccount(const OCC::AccountState * const accountState);

Expand Down
78 changes: 62 additions & 16 deletions src/gui/macOS/fileproviderdomainmanager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
#include <QLatin1StringView>
#include <QList>
#include <QLoggingCategory>
#include <QSet>
#include <QUuid>

#include "config.h"
#include "fileprovider.h"
#include "fileproviderdomainidentifierpolicy.h"
#include "fileproviderdomainmanager.h"
#include "fileprovidersettingscontroller.h"
#include "fileproviderutils.h"
Expand Down Expand Up @@ -100,16 +102,29 @@ void disconnect(NSFileProviderDomain *domain, const QString &message)
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
}

/**
* @brief Result of listing domains from `NSFileProviderManager`.
*
* `succeeded` is false when the system reported an error. In that case `domains` is empty
* and must not be treated as "there are no domains".
*/
struct DomainListing
{
bool succeeded = false;
QList<NSFileProviderDomain *> domains;
};

/**
* @brief Synchronous and logging wrapper for `[NSFileProviderManager getDomainsWithCompletionHandler:]`.
*/
QList<NSFileProviderDomain *> getDomains()
DomainListing listDomains()
{
qCInfo(lcMacFileProviderDomainManager) << "Getting all existing domains...";
dispatch_group_t dispatchGroup = dispatch_group_create();
dispatch_group_enter(dispatchGroup);

__block NSArray<NSFileProviderDomain *> *returnValue = [NSArray array];
__block auto listingSucceeded = false;

[NSFileProviderManager getDomainsWithCompletionHandler:^(NSArray<NSFileProviderDomain *> * const domains, NSError * const error) {
if (error) {
Expand All @@ -130,18 +145,31 @@ void disconnect(NSFileProviderDomain *domain, const QString &message)

// Ensure the array (and contained domains) stay retained after the completion block returns.
returnValue = [domains copy];
listingSucceeded = true;
dispatch_group_leave(dispatchGroup);
}];

dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);

QList<NSFileProviderDomain *> domainsList;
auto listing = DomainListing{};
listing.succeeded = listingSucceeded;

for (NSFileProviderDomain * const domain in returnValue) {
domainsList.append(domain);
listing.domains.append(domain);
}

return domainsList;
return listing;
}

/**
* @brief Identifiers of domains currently registered with the system.
*
* Empty when listing failed. Callers that need to distinguish "none" from "unknown"
* must use `listDomains()` instead.
*/
QList<NSFileProviderDomain *> getDomains()
{
return listDomains().domains;
}

/**
Expand Down Expand Up @@ -392,22 +420,40 @@ QString addFileProviderDomain(const AccountState * const accountState)
// `reconcileDomainDisplayNames()`.
const auto domainDisplayName = account->shortcutName();

if (!existingDomainId.isEmpty()) {
const auto domains = getDomains();
const auto listing = listDomains();
auto registeredIdentifiers = QSet<QString>{};

for (NSFileProviderDomain * const domain : domains) {
if (existingDomainId == QString::fromNSString(domain.identifier)) {
qCDebug(lcMacFileProviderDomainManager) << "Domain already exists for account"
<< accountId
<< "with identifier"
<< existingDomainId;

return existingDomainId;
}
if (listing.succeeded) {
for (NSFileProviderDomain * const domain : listing.domains) {
registeredIdentifiers.insert(QString::fromNSString(domain.identifier));
}
}

const auto domainId = QUuid::createUuid().toString(QUuid::WithoutBraces);
const auto decision = FileProviderDomainIdentifierPolicy::decideRegistration(existingDomainId,
registeredIdentifiers,
listing.succeeded);

if (decision.action == FileProviderDomainIdentifierPolicy::RegistrationAction::Abort) {
qCWarning(lcMacFileProviderDomainManager) << "Not adding a file provider domain for account"
<< accountId
<< "because listing existing domains failed; refusing to mint a new identifier.";
return {};
}

if (decision.action == FileProviderDomainIdentifierPolicy::RegistrationAction::Skip) {
qCDebug(lcMacFileProviderDomainManager) << "Domain already exists for account"
<< accountId
<< "with identifier"
<< existingDomainId;
return existingDomainId;
}

auto domainId = existingDomainId;

if (decision.action == FileProviderDomainIdentifierPolicy::RegistrationAction::AddFresh) {
domainId = QUuid::createUuid().toString(QUuid::WithoutBraces);
}

NSFileProviderDomain * const domain = [[NSFileProviderDomain alloc] initWithIdentifier:domainId.toNSString() displayName:domainDisplayName.toNSString()];
domain.supportsSyncingTrash = YES;

Expand Down
9 changes: 4 additions & 5 deletions src/gui/macOS/fileprovidersettingscontroller_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,10 @@ explicit MacImplementation(FileProviderSettingsController *const parent)

if (setEnabled) {
// addDomainForAccount is idempotent: it returns the existing identifier when
// the domain is still registered with the system, re-creates it when the
// stored identifier is stale (the "fake" identifiers minted by
// migrateToAppSandbox, or a domain the user removed in System Settings), or
// creates a fresh one. Always going through it guarantees a real domain
// exists before the caller discards the classic sync folders.
// the domain is still registered, re-adds the same stored identifier when the
// domain is missing (sandbox-migration placeholders, or a domain removed in
// System Settings), or mints a fresh UUID only when the account has none.
// A failed domain listing aborts instead of minting a second identifier.
auto const identifier = Mac::FileProvider::instance()->domainManager()->addDomainForAccount(accountState.data());

if (identifier.isEmpty()) {
Expand Down
1 change: 1 addition & 0 deletions test/macOS/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ nextcloud_add_test(MacSandboxUtility)
if(BUILD_FILE_PROVIDER_MODULE)
nextcloud_add_test(FileProviderXPCUtils)
set_source_files_properties(testfileproviderxpcutils.cpp PROPERTIES COMPILE_FLAGS "-x objective-c++")
nextcloud_add_test(FileProviderDomainIdentifierPolicy)
endif()
64 changes: 64 additions & 0 deletions test/macOS/testfileproviderdomainidentifierpolicy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*
* This software is in the public domain, furnished "as is", without technical
* support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
*/

#include <QtTest>

#include "macOS/fileproviderdomainidentifierpolicy.h"

using namespace OCC::Mac::FileProviderDomainIdentifierPolicy;

class TestFileProviderDomainIdentifierPolicy : public QObject
{
Q_OBJECT

private Q_SLOTS:
void listingFailureDoesNotMintANewIdentifier()
{
const auto stored = QStringLiteral("0bd6be4e-6151-4db4-9668-57d8503d6d3f");
const auto decision = decideRegistration(stored, {}, false);
QCOMPARE(decision.action, RegistrationAction::Abort);
}

void listingFailureWithNoStoredIdentifierAborts()
{
const auto decision = decideRegistration({}, {}, false);
QCOMPARE(decision.action, RegistrationAction::Abort);
}

void storedIdentifierAlreadyRegisteredIsSkipped()
{
const auto stored = QStringLiteral("b375bcfe-1653-457b-ab49-fca678c8cd6d");
const auto decision = decideRegistration(stored, {stored}, true);
QCOMPARE(decision.action, RegistrationAction::Skip);
}

void vanishedStoredIdentifierIsReusedNotReplaced()
{
const auto stored = QStringLiteral("0bd6be4e-6151-4db4-9668-57d8503d6d3f");
const auto other = QStringLiteral("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
const auto decision = decideRegistration(stored, {other}, true);
QCOMPARE(decision.action, RegistrationAction::AddStored);
}

void emptyListingWithStoredIdentifierReusesStored()
{
const auto stored = QStringLiteral("0bd6be4e-6151-4db4-9668-57d8503d6d3f");
const auto decision = decideRegistration(stored, {}, true);
QCOMPARE(decision.action, RegistrationAction::AddStored);
}

void firstEnableMintsAFreshIdentifier()
{
const auto decision = decideRegistration({}, {}, true);
QCOMPARE(decision.action, RegistrationAction::AddFresh);
}
};

QTEST_APPLESS_MAIN(TestFileProviderDomainIdentifierPolicy)
#include "testfileproviderdomainidentifierpolicy.moc"