Skip to content

Commit bec212b

Browse files
committed
fix(account/setup): repair account creation via CLI
Setting up an account with --userid/--apppassword/--serverurl never succeeded on macOS. Four independent defects were involved: - Options were only accepted as "--option value". Argument lists are now normalised with Utility::expandCommandLineOptionValues(), so the "--option=value" spelling works for every option in both the client and nextcloudcmd. The latter has to normalise before it checks for --userid, which is what selects provisioning mode. - --localdirpath is documented as optional but an empty one was rejected outright, although the setup already had a branch for accounts without a folder. It now falls back to the folder the account wizard would suggest. - FolderMan::addFolder() refuses to create a classic sync folder while the app-level File Provider mode is enabled, yet the setup always tried to create one, so it always failed. localSyncFolderRequired() now mirrors that guard and the account is set up without a folder in that mode. - 09f4692 added the account and its sync folder before the credentials were checked. Setting up the folder ends by scheduling qApp->quit(), so the credential check, the account save and the keychain write never ran. The account is now only added once the credentials validate, which also means a failed setup no longer leaves a half-written account behind. Two related fixes: a failed folder setup called deleteAccount(), revoking the app password that was passed on the command line, and the client overwrote the configured VFS setting on every start because the account setup parser instance is never null. nextcloudcmd now runs the event loop so the asynchronous setup can finish. Its keychain jobs never signal completion, which the previous immediate return hid; the wait for the credentials is bounded so provisioning ends with a warning instead of hanging. Signed-off-by: Iva Horn <iva.horn@nextcloud.com>
1 parent 5745792 commit bec212b

6 files changed

Lines changed: 195 additions & 39 deletions

File tree

src/cmd/cmd.cpp

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ CommandMode parseOptions(const QStringList &app_args, CmdOptions *options)
228228
{
229229
auto result = CommandMode::UnknownMode;
230230

231-
auto args(app_args);
231+
// Accept both "--option value" and "--option=value" for every option below.
232+
auto args = Utility::expandCommandLineOptionValues(app_args);
232233

233234
const auto argCount = args.count();
234235

@@ -446,19 +447,22 @@ int main(int argc, char **argv)
446447
return -1;
447448
}
448449

449-
if (AccountSetupCommandLineManager::instance()->isCommandLineParsed()) {
450-
if (AccountSetupCommandLineManager::instance()->setupAccountFromCommandLine()) {
451-
return 0;
452-
} else {
453-
qWarning() << "Creation of the account failed. See prior messages for a detailed error.";
454-
return -1;
455-
}
456-
} else {
450+
if (!AccountSetupCommandLineManager::instance()->isCommandLineParsed()) {
457451
AccountSetupCommandLineManager::destroy();
458452
qWarning() << "Missing mandatory command line options for provisioning mode";
459453
help();
460454
return -1;
461455
}
456+
457+
if (!AccountSetupCommandLineManager::instance()->setupAccountFromCommandLine()) {
458+
qWarning() << "Creation of the account failed. See prior messages for a detailed error.";
459+
return -1;
460+
}
461+
462+
// Setting up the account validates the credentials against the server and stores
463+
// them in the keychain, both of which are asynchronous. The setup job ends the
464+
// event loop with the exit code once it has finished.
465+
return app.exec();
462466
}
463467

464468
AccountPtr account = Account::create();

src/common/utility.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,5 +681,28 @@ QString Utility::fullRemotePathToRemoteSyncRootRelative(const QString &fullRemot
681681
return noLeadingSlashPath(noTrailingSlashPath(relativePathToRemoteSyncRoot));
682682
}
683683

684+
QStringList Utility::expandCommandLineOptionValues(const QStringList &arguments)
685+
{
686+
QStringList expandedArguments;
687+
expandedArguments.reserve(arguments.size());
688+
689+
for (const auto &argument : arguments) {
690+
// Anything that is not a long option is passed through untouched: paths and custom
691+
// URI scheme arguments may legitimately contain a '='.
692+
const auto separator = argument.startsWith(QLatin1String("--")) ? argument.indexOf(QLatin1Char('=')) : -1;
693+
if (separator < 3) {
694+
expandedArguments.append(argument);
695+
continue;
696+
}
697+
698+
expandedArguments.append(argument.left(separator));
699+
700+
if (const auto value = argument.mid(separator + 1); !value.isEmpty()) {
701+
expandedArguments.append(value);
702+
}
703+
}
704+
705+
return expandedArguments;
706+
}
684707

685708
} // namespace OCC

src/common/utility.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,19 @@ namespace Utility {
336336
OCSYNC_EXPORT QString noTrailingSlashPath(const QString &path);
337337
OCSYNC_EXPORT QString fullRemotePathToRemoteSyncRootRelative(const QString &fullRemotePath, const QString &remoteSyncRoot);
338338

339+
/**
340+
* @brief Splits "--option=value" arguments into a separate "--option" and "value" entry
341+
*
342+
* The option parsers of the client and of nextcloudcmd walk the argument list with an
343+
* iterator and expect the value of an option in the entry that follows it. Normalising
344+
* the list before parsing makes both spellings work without teaching every single
345+
* option about the inline form.
346+
*
347+
* Only entries starting with "--" are split, and only at their first '='. "--option="
348+
* keeps no value so that the parsers report their usual "not specified" error.
349+
*/
350+
OCSYNC_EXPORT QStringList expandCommandLineOptionValues(const QStringList &arguments);
351+
339352
#ifdef Q_OS_WIN
340353
OCSYNC_EXPORT bool registryKeyExists(HKEY hRootKey, const QString &subKey);
341354
OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);

src/gui/accountsetupfromcommandlinejob.cpp

Lines changed: 120 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,34 @@
77

88
#include "accountmanager.h"
99
#include "accountstate.h"
10+
#include "common/filesystembase.h"
11+
#include "configfile.h"
1012
#include "creds/abstractcredentials.h"
1113
#include "creds/webflowcredentials.h"
12-
#include "common/filesystembase.h"
1314
#include "folder.h"
1415
#include "folderman.h"
1516
#include "networkjobs.h"
17+
#include "theme.h"
18+
19+
#include <chrono>
20+
#include <iostream>
1621

1722
#include <QDir>
1823
#include <QGuiApplication>
1924
#include <QJsonObject>
2025
#include <QLoggingCategory>
26+
#include <QTimer>
2127

2228
using namespace Qt::StringLiterals;
2329

2430
namespace OCC
2531
{
2632
Q_LOGGING_CATEGORY(lcAccountSetupCommandLineJob, "nextcloud.gui.accountsetupcommandlinejob", QtInfoMsg)
2733

34+
// How long to wait for the keychain to confirm that the credentials were written before
35+
// finishing the setup without them.
36+
constexpr auto credentialsPersistTimeout = std::chrono::seconds(30);
37+
2838
AccountSetupFromCommandLineJob::AccountSetupFromCommandLineJob(QString appPassword,
2939
QString userId,
3040
QUrl serverUrl,
@@ -42,37 +52,77 @@ AccountSetupFromCommandLineJob::AccountSetupFromCommandLineJob(QString appPasswo
4252
{
4353
}
4454

55+
bool AccountSetupFromCommandLineJob::localSyncFolderRequired() const
56+
{
57+
#ifdef BUILD_FILE_PROVIDER_MODULE
58+
// Mirrors the guard in FolderMan::addFolder(): while the app-level File Provider mode
59+
// is enabled, a classic sync folder cannot be created at all. The account is synced
60+
// through the File Provider domain that is set up for it instead.
61+
return !ConfigFile().macFileProviderModeEnabled();
62+
#else
63+
return true;
64+
#endif
65+
}
66+
67+
QString AccountSetupFromCommandLineJob::defaultLocalDirPath() const
68+
{
69+
const auto overrideLocalDir = ConfigFile().overrideLocalDir();
70+
auto localDirPath = overrideLocalDir;
71+
72+
if (localDirPath.isEmpty()) {
73+
localDirPath = Theme::instance()->defaultClientFolder();
74+
75+
if (localDirPath.isEmpty()) {
76+
return {};
77+
}
78+
79+
if (!QDir(localDirPath).isAbsolute()) {
80+
localDirPath = QDir::homePath() + QLatin1Char('/') + localDirPath;
81+
}
82+
}
83+
84+
auto serverUrlForFolder = _serverUrl;
85+
serverUrlForFolder.setUserName(_userId);
86+
87+
return FolderMan::instance()->findGoodPathForNewSyncFolder(localDirPath,
88+
serverUrlForFolder,
89+
overrideLocalDir.isEmpty() ? FolderMan::GoodPathStrategy::AllowOnlyNewPath
90+
: FolderMan::GoodPathStrategy::AllowOverrideExistingPath);
91+
}
92+
4593
bool AccountSetupFromCommandLineJob::handleAccountSetupFromCommandLine()
4694
{
4795
if (AccountManager::instance()->accountFromUserId(QStringLiteral("%1@%2").arg(_userId, _serverUrl.host()))) {
4896
printAccountSetupFromCommandLineStatusAndExit(QStringLiteral("Account %1 already exists!").arg(QDir::toNativeSeparators(_userId)), true);
4997
return false;
5098
}
5199

52-
if (_localDirPath.isEmpty()) {
53-
printAccountSetupFromCommandLineStatusAndExit(
54-
QStringLiteral("Folder creation failed. Could not create local folder because the name is empty"),
55-
true);
56-
return false;
100+
if (!localSyncFolderRequired()) {
101+
if (!_localDirPath.isEmpty()) {
102+
qCInfo(lcAccountSetupCommandLineJob) << "Ignoring the given local folder, File Provider mode is enabled";
103+
_localDirPath.clear();
104+
}
57105
} else {
58-
QDir dir(_localDirPath);
59-
if (dir.exists() && !dir.isEmpty()) {
106+
if (_localDirPath.isEmpty()) {
107+
// The local folder is documented as optional, so fall back to the folder the
108+
// account wizard would suggest rather than refusing to set the account up.
109+
_localDirPath = defaultLocalDirPath();
110+
}
111+
112+
if (_localDirPath.isEmpty()) {
60113
printAccountSetupFromCommandLineStatusAndExit(
61-
QStringLiteral("Local folder %1 already exists and is non-empty!").arg(QDir::toNativeSeparators(_localDirPath)),
114+
QStringLiteral("Could not determine a local folder to sync into. Please pass one with --localdirpath."),
62115
true);
63116
return false;
64117
}
65118

66-
qCInfo(lcAccountSetupCommandLineJob) << "Creating folder" << _localDirPath;
67-
if (!dir.exists() && !dir.mkpath(".")) {
119+
const QDir localDir(_localDirPath);
120+
if (localDir.exists() && !localDir.isEmpty()) {
68121
printAccountSetupFromCommandLineStatusAndExit(
69-
QStringLiteral("Folder creation failed. Could not create local folder %1").arg(QDir::toNativeSeparators(_localDirPath)),
122+
QStringLiteral("Local folder %1 already exists and is non-empty!").arg(QDir::toNativeSeparators(_localDirPath)),
70123
true);
71124
return false;
72125
}
73-
74-
FileSystem::setFolderMinimumPermissions(_localDirPath);
75-
Utility::setupFavLink(_localDirPath);
76126
}
77127

78128
const auto credentials = new WebFlowCredentials(_userId, _appPassword);
@@ -81,12 +131,16 @@ bool AccountSetupFromCommandLineJob::handleAccountSetupFromCommandLine()
81131
_account->setCredentials(credentials);
82132
_account->setCredentialSetting(u"user"_s, _userId);
83133
_account->setUrl(_serverUrl);
84-
auto accountState = AccountManager::instance()->addAccount(_account);
85-
setupLocalSyncFolder(accountState);
86-
87-
Q_EMIT _account->wantsAccountSaved(_account);
88134

135+
// The account is only added, saved and given a sync folder once the credentials have
136+
// been checked against the server, so that a failed setup leaves nothing behind. Both
137+
// that check and the keychain write are asynchronous: the job keeps running until
138+
// printAccountSetupFromCommandLineStatusAndExit() ends the event loop.
89139
if (_appPassword.isEmpty()) {
140+
// Nothing to authenticate with, so the server cannot be asked for the dav user
141+
// either. Store what was given and let the user log in from the client later on.
142+
_account->setDavUser(_userId);
143+
accountSetupFromCommandLinePropfindHandleSuccess();
90144
return true;
91145
}
92146

@@ -112,19 +166,40 @@ void AccountSetupFromCommandLineJob::accountSetupFromCommandLinePropfindHandleSu
112166
const auto accountManager = AccountManager::instance();
113167
const auto accountState = accountManager->addAccount(_account);
114168

115-
// credentials->persist() (called by save()) is asynchronous — it chains
116-
// multiple keychain write jobs before the password actually lands in the
117-
// keychain. Wait for the final write to complete before exiting so that
118-
// the credentials are not lost when the process quits.
119-
connect(_account->credentials(), &AbstractCredentials::credentialsPersisted, this, [this, accountState]() {
169+
const auto finishAccountSetup = [this, accountState]() {
120170
if (!_localDirPath.isEmpty()) {
121171
setupLocalSyncFolder(accountState);
122172
} else {
123173
qCInfo(lcAccountSetupCommandLineJob) << QStringLiteral("Set up a new account without a folder.");
124174
printAccountSetupFromCommandLineStatusAndExit(QStringLiteral("Account %1 setup from command line success.").arg(_account->displayName()), false);
125175
}
176+
};
177+
178+
// credentials->persist() (called by save()) is asynchronous — it chains
179+
// multiple keychain write jobs before the password actually lands in the
180+
// keychain. Wait for the final write to complete before exiting so that
181+
// the credentials are not lost when the process quits, but give up eventually:
182+
// a keychain that never answers must not turn the setup into a hang.
183+
const auto credentialsPersistTimer = new QTimer(this);
184+
credentialsPersistTimer->setSingleShot(true);
185+
186+
connect(_account->credentials(), &AbstractCredentials::credentialsPersisted, this, [credentialsPersistTimer, finishAccountSetup]() {
187+
if (!credentialsPersistTimer->isActive()) {
188+
return;
189+
}
190+
191+
credentialsPersistTimer->stop();
192+
finishAccountSetup();
193+
});
194+
195+
connect(credentialsPersistTimer, &QTimer::timeout, this, [finishAccountSetup]() {
196+
qCWarning(lcAccountSetupCommandLineJob) << "Timed out waiting for the credentials to be written to the keychain,"
197+
<< "the account may have to be authenticated again";
198+
finishAccountSetup();
126199
});
127200

201+
credentialsPersistTimer->start(credentialsPersistTimeout);
202+
128203
accountManager->save();
129204
}
130205

@@ -183,6 +258,22 @@ void AccountSetupFromCommandLineJob::accountSetupFromCommandLinePropfindHandleFa
183258

184259
void AccountSetupFromCommandLineJob::setupLocalSyncFolder(AccountState *accountState)
185260
{
261+
QDir localDir(_localDirPath);
262+
if (!localDir.exists()) {
263+
qCInfo(lcAccountSetupCommandLineJob) << "Creating folder" << _localDirPath;
264+
265+
if (!localDir.mkpath(QStringLiteral("."))) {
266+
AccountManager::instance()->removeAccountState(accountState);
267+
printAccountSetupFromCommandLineStatusAndExit(
268+
QStringLiteral("Folder creation failed. Could not create local folder %1").arg(QDir::toNativeSeparators(_localDirPath)),
269+
true);
270+
return;
271+
}
272+
}
273+
274+
FileSystem::setFolderMinimumPermissions(_localDirPath);
275+
Utility::setupFavLink(_localDirPath);
276+
186277
FolderDefinition definition;
187278
definition.localPath = _localDirPath;
188279
definition.targetPath = FolderDefinition::prepareTargetPath(!_remoteDirPath.isEmpty() ? _remoteDirPath : QStringLiteral("/"));
@@ -209,7 +300,9 @@ void AccountSetupFromCommandLineJob::setupLocalSyncFolder(AccountState *accountS
209300
qCInfo(lcAccountSetupCommandLineJob) << QStringLiteral("Folder %1 setup from command line success.").arg(definition.localPath);
210301
printAccountSetupFromCommandLineStatusAndExit(QStringLiteral("Account %1 setup from command line success.").arg(_account->displayName()), false);
211302
} else {
212-
AccountManager::instance()->deleteAccount(accountState);
303+
// Drop the account again, but keep the app password valid: it was passed in on the
304+
// command line and revoking it would make a retry impossible.
305+
AccountManager::instance()->removeAccountState(accountState);
213306
printAccountSetupFromCommandLineStatusAndExit(
214307
QStringLiteral("Account %1 setup from command line failed, due to folder creation failure.").arg(_account->displayName()),
215308
true);
@@ -220,8 +313,10 @@ void AccountSetupFromCommandLineJob::printAccountSetupFromCommandLineStatusAndEx
220313
{
221314
if (isFailure) {
222315
qCWarning(lcAccountSetupCommandLineJob) << status;
316+
std::cerr << qUtf8Printable(status) << std::endl;
223317
} else {
224318
qCInfo(lcAccountSetupCommandLineJob) << status;
319+
std::cout << qUtf8Printable(status) << std::endl;
225320
}
226321
QTimer::singleShot(0, this, [this, isFailure]() {
227322
this->deleteLater();

src/gui/accountsetupfromcommandlinejob.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class AccountSetupFromCommandLineJob : public QObject
2424
QString userId,
2525
QUrl serverUrl,
2626
QString localDirPath = {},
27-
bool nonVfsMode = false,
27+
bool isVfsEnabled = false,
2828
QString remoteDirPath = QStringLiteral("/"),
2929
QObject *parent = nullptr);
3030

@@ -45,6 +45,20 @@ private Q_SLOTS:
4545
void fetchUserName();
4646

4747
private:
48+
/** Whether a classic sync folder has to be set up for the new account.
49+
*
50+
* With the app-level File Provider mode enabled the account is synced through its
51+
* File Provider domain and FolderMan refuses to add a classic sync folder.
52+
*/
53+
[[nodiscard]] bool localSyncFolderRequired() const;
54+
55+
/** The local folder to use when none was given on the command line.
56+
*
57+
* Follows what the account wizard suggests: the configured override, otherwise the
58+
* theme's default client folder, made unique against the existing sync folders.
59+
*/
60+
[[nodiscard]] QString defaultLocalDirPath() const;
61+
4862
QString _appPassword;
4963
QString _userId;
5064
QUrl _serverUrl;

src/gui/application.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727
#include "updater/ocupdater.h"
2828
#endif
2929

30+
#include "common/utility.h"
31+
#include "common/vfs.h"
32+
#include "csync_exclude.h"
3033
#include "owncloudsetupwizard.h"
3134
#include "version.h"
32-
#include "csync_exclude.h"
33-
#include "common/vfs.h"
3435

3536
#include "config.h"
3637

@@ -389,7 +390,10 @@ Application::Application(int &argc, char **argv)
389390
shouldExit = true;
390391
}
391392

392-
if (AccountSetupCommandLineManager::instance()) {
393+
// Only a command line that actually provisions an account carries a meaningful
394+
// --isvfsenabled value; on a normal start this would overwrite the user's setting
395+
// with the default of an unused parser.
396+
if (AccountSetupCommandLineManager::instance()->isCommandLineParsed()) {
393397
cfg.setVfsEnabled(AccountSetupCommandLineManager::instance()->isVfsEnabled());
394398
}
395399

@@ -1006,7 +1010,10 @@ void Application::slotActivateRequestedMessage(const QStringList &arguments, con
10061010

10071011
void Application::parseOptions(const QStringList &options)
10081012
{
1009-
QStringListIterator it(options);
1013+
// Accept both "--option value" and "--option=value" for every option below.
1014+
const auto expandedOptions = Utility::expandCommandLineOptionValues(options);
1015+
1016+
QStringListIterator it(expandedOptions);
10101017
// skip file name;
10111018
if (it.hasNext()) {
10121019
it.next();

0 commit comments

Comments
 (0)