diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 7588a6169..af02aa429 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -65,6 +65,10 @@ set(client_SRCS accountmanager.cpp accountsettings.h accountsettings.cpp + accountsetupfromcommandlinejob.h + accountsetupfromcommandlinejob.cpp + accountsetupcommandlinemanager.h + accountsetupcommandlinemanager.cpp application.h application.cpp invalidfilenamedialog.h diff --git a/src/gui/accountsetupcommandlinemanager.cpp b/src/gui/accountsetupcommandlinemanager.cpp new file mode 100644 index 000000000..a604cb53b --- /dev/null +++ b/src/gui/accountsetupcommandlinemanager.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 "accountsetupcommandlinemanager.h" +#include "accountsetupfromcommandlinejob.h" + +namespace OCC +{ +Q_LOGGING_CATEGORY(lcAccountSetupCommandLineManager, "nextcloud.gui.accountsetupcommandlinemanager", QtInfoMsg) + +AccountSetupCommandLineManager *AccountSetupCommandLineManager::_instance = nullptr; + +AccountSetupCommandLineManager::AccountSetupCommandLineManager(QObject *parent) + : QObject{parent} +{ +} + +AccountSetupCommandLineManager *AccountSetupCommandLineManager::instance() +{ + if (!_instance) { + _instance = new AccountSetupCommandLineManager(); + } + return _instance; +} + +void AccountSetupCommandLineManager::destroy() +{ + if (_instance) { + _instance->deleteLater(); + _instance = nullptr; + } +} + +bool AccountSetupCommandLineManager::parseCommandlineOption(const QString &option, QStringListIterator &optionsIterator, QString &errorMessage) +{ + if (option == QStringLiteral("--apppassword")) { + if (optionsIterator.hasNext() && !optionsIterator.peekNext().startsWith(QLatin1String("--"))) { + _appPassword = optionsIterator.next(); + return true; + } else { + errorMessage = QStringLiteral("apppassword not specified"); + } + } else if (option == QStringLiteral("--localdirpath")) { + if (optionsIterator.hasNext() && !optionsIterator.peekNext().startsWith(QLatin1String("--"))) { + _localDirPath = optionsIterator.next(); + return true; + } else { + errorMessage = QStringLiteral("basedir not specified"); + } + } else if (option == QStringLiteral("--remotedirpath")) { + if (optionsIterator.hasNext() && !optionsIterator.peekNext().startsWith(QLatin1String("--"))) { + _remoteDirPath = optionsIterator.next(); + return true; + } else { + errorMessage = QStringLiteral("remotedir not specified"); + } + } else if (option == QStringLiteral("--serverurl")) { + if (optionsIterator.hasNext() && !optionsIterator.peekNext().startsWith(QLatin1String("--"))) { + _serverUrl = optionsIterator.next(); + return true; + } else { + errorMessage = QStringLiteral("serverurl not specified"); + } + } else if (option == QStringLiteral("--userid")) { + if (optionsIterator.hasNext() && !optionsIterator.peekNext().startsWith(QLatin1String("--"))) { + _userId = optionsIterator.next(); + return true; + } else { + errorMessage = QStringLiteral("userid not specified"); + } + } else if (option == QLatin1String("--isvfsenabled")) { + if (optionsIterator.hasNext() && !optionsIterator.peekNext().startsWith(QLatin1String("--"))) { + _isVfsEnabled = optionsIterator.next().toInt() != 0; + return true; + } else { + errorMessage = QStringLiteral("isvfsenabled not specified"); + } + } + return false; +} + +bool AccountSetupCommandLineManager::isCommandLineParsed() const +{ + return !_appPassword.isEmpty() && !_userId.isEmpty() && _serverUrl.isValid(); +} + +void AccountSetupCommandLineManager::setupAccountFromCommandLine() +{ + if (isCommandLineParsed()) { + qCInfo(lcAccountSetupCommandLineManager) << QStringLiteral("Command line has been parsed and account setup parameters have been found. Attempting setup a new account %1...").arg(_userId); + const auto accountSetupJob = new AccountSetupFromCommandLineJob(_appPassword, _userId, _serverUrl, _localDirPath, _isVfsEnabled, _remoteDirPath, parent()); + accountSetupJob->handleAccountSetupFromCommandLine(); + } else { + qCInfo(lcAccountSetupCommandLineManager) << QStringLiteral("No account setup parameters have been found, or they are invalid. Proceed with normal startup..."); + } + _appPassword.clear(); + _userId.clear(); + _serverUrl.clear(); + _remoteDirPath.clear(); + _localDirPath.clear(); + _isVfsEnabled = true; +} +} diff --git a/src/gui/accountsetupcommandlinemanager.h b/src/gui/accountsetupcommandlinemanager.h new file mode 100644 index 000000000..585627e85 --- /dev/null +++ b/src/gui/accountsetupcommandlinemanager.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +namespace OCC { +class AccountSetupCommandLineManager : public QObject +{ + Q_OBJECT + +public: + [[nodiscard]] static AccountSetupCommandLineManager *instance(); + static void destroy(); + + [[nodiscard]] bool parseCommandlineOption(const QString &option, QStringListIterator &optionsIterator, QString &errorMessage); + + [[nodiscard]] bool isCommandLineParsed() const; + +public slots: + void setupAccountFromCommandLine(); + +private: + explicit AccountSetupCommandLineManager(QObject *parent = nullptr); + + static AccountSetupCommandLineManager *_instance; + + QString _appPassword; + QString _userId; + QUrl _serverUrl; + QString _remoteDirPath; + QString _localDirPath; + bool _isVfsEnabled; +}; + +} diff --git a/src/gui/accountsetupfromcommandlinejob.cpp b/src/gui/accountsetupfromcommandlinejob.cpp new file mode 100644 index 000000000..eeff0cfb0 --- /dev/null +++ b/src/gui/accountsetupfromcommandlinejob.cpp @@ -0,0 +1,238 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 "accountsetupfromcommandlinejob.h" + +#include "accountmanager.h" +#include "accountstate.h" +#include "creds/webflowcredentials.h" +#include "filesystem.h" +#include "folder.h" +#include "folderman.h" +#include "networkjobs.h" + +#include +#include +#include +#include + +namespace OCC +{ +Q_LOGGING_CATEGORY(lcAccountSetupCommandLineJob, "nextcloud.gui.accountsetupcommandlinejob", QtInfoMsg) + +AccountSetupFromCommandLineJob::AccountSetupFromCommandLineJob(QString appPassword, + QString userId, + QUrl serverUrl, + QString localDirPath, + bool isVfsEnabled, + QString remoteDirPath, + QObject *parent) + : QObject(parent) + , _appPassword(appPassword) + , _userId(userId) + , _serverUrl(serverUrl) + , _localDirPath(localDirPath) + , _isVfsEnabled(isVfsEnabled) + , _remoteDirPath(remoteDirPath) +{ +} + +void AccountSetupFromCommandLineJob::handleAccountSetupFromCommandLine() +{ + if (AccountManager::instance()->accountFromUserId(QStringLiteral("%1@%2").arg(_userId).arg(_serverUrl.host()))) { + printAccountSetupFromCommandLineStatusAndExit(QStringLiteral("Account %1 already exists!").arg(QDir::toNativeSeparators(_userId)), true); + return; + } + + if (!_localDirPath.isEmpty()) { + QDir dir(_localDirPath); + if (dir.exists() && !dir.isEmpty()) { + printAccountSetupFromCommandLineStatusAndExit( + QStringLiteral("Local folder %1 already exists and is non-empty!").arg(QDir::toNativeSeparators(_localDirPath)), + true); + return; + } + + qCInfo(lcAccountSetupCommandLineJob) << "Creating folder" << _localDirPath; + if (!dir.exists() && !dir.mkpath(".")) { + printAccountSetupFromCommandLineStatusAndExit( + QStringLiteral("Folder creation failed. Could not create local folder %1").arg(QDir::toNativeSeparators(_localDirPath)), + true); + return; + } + + FileSystem::setFolderMinimumPermissions(_localDirPath); + Utility::setupFavLink(_localDirPath); + } + + const auto credentials = new WebFlowCredentials(_userId, _appPassword); + _account = AccountManager::createAccount(); + _account->setCredentials(credentials); + _account->setUrl(_serverUrl); + + fetchUserName(); +} + +void AccountSetupFromCommandLineJob::checkLastModifiedWithPropfind() +{ + const auto job = new PropfindJob(_account, "/", this); + job->setIgnoreCredentialFailure(true); + // There is custom redirect handling in the error handler, + // so don't automatically follow redirects. + job->setFollowRedirects(false); + job->setProperties(QList() << QByteArrayLiteral("getlastmodified")); + connect(job, &PropfindJob::result, this, &AccountSetupFromCommandLineJob::accountSetupFromCommandLinePropfindHandleSuccess); + connect(job, &PropfindJob::finishedWithError, this, &AccountSetupFromCommandLineJob::accountSetupFromCommandLinePropfindHandleFailure); + job->start(); +} + +void AccountSetupFromCommandLineJob::accountSetupFromCommandLinePropfindHandleSuccess() +{ + const auto accountManager = AccountManager::instance(); + const auto accountState = accountManager->addAccount(_account); + accountManager->save(); + + if (!_localDirPath.isEmpty()) { + setupLocalSyncFolder(accountState); + } else { + qCInfo(lcAccountSetupCommandLineJob) << QStringLiteral("Set up a new account without a folder."); + printAccountSetupFromCommandLineStatusAndExit(QStringLiteral("Account %1 setup from command line success.").arg(_account->displayName()), false); + } +} + +void AccountSetupFromCommandLineJob::accountSetupFromCommandLinePropfindHandleFailure() +{ + const auto job = qobject_cast(sender()); + if (!job) { + printAccountSetupFromCommandLineStatusAndExit(QStringLiteral("Cannot check for authed redirects. This slot should be invoked from PropfindJob!"), true); + return; + } + const auto reply = job->reply(); + + QString errorMsg; + + // If there were redirects on the *authed* requests, also store + // the updated server URL, similar to redirects on status.php. + QUrl redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + if (!redirectUrl.isEmpty()) { + qCInfo(lcAccountSetupCommandLineJob) << "Authed request was redirected to" << redirectUrl.toString(); + + // strip the expected path + auto path = redirectUrl.path(); + static QString expectedPath = "/" + _account->davPath(); + if (path.endsWith(expectedPath)) { + path.chop(expectedPath.size()); + redirectUrl.setPath(path); + + qCInfo(lcAccountSetupCommandLineJob) << "Setting account url to" << redirectUrl.toString(); + _account->setUrl(redirectUrl); + checkLastModifiedWithPropfind(); + } + errorMsg = tr("The authenticated request to the server was redirected to " + "\"%1\". The URL is bad, the server is misconfigured.") + .arg(Utility::escape(redirectUrl.toString())); + + // A 404 is actually a success: we were authorized to know that the folder does + // not exist. It will be created later... + } else if (reply->error() == QNetworkReply::ContentNotFoundError) { + accountSetupFromCommandLinePropfindHandleSuccess(); + } else if (reply->error() != QNetworkReply::NoError) { + if (!_account->credentials()->stillValid(reply)) { + errorMsg = tr("Access forbidden by server. To verify that you have proper access, " + "click here to access the service with your browser.") + .arg(Utility::escape(_account->url().toString())); + } else { + errorMsg = job->errorStringParsingBody(); + } + // Something else went wrong, maybe the response was 200 but with invalid data. + } else { + errorMsg = tr("There was an invalid response to an authenticated WebDAV request"); + } + printAccountSetupFromCommandLineStatusAndExit( + QStringLiteral("Account %1 setup from command line failed with error: %2.").arg(_account->displayName()).arg(errorMsg), + true); +} + +void AccountSetupFromCommandLineJob::setupLocalSyncFolder(AccountState *accountState) +{ + FolderDefinition definition; + definition.localPath = _localDirPath; + definition.targetPath = FolderDefinition::prepareTargetPath(!_remoteDirPath.isEmpty() ? _remoteDirPath : QStringLiteral("/")); + definition.virtualFilesMode = _isVfsEnabled ? bestAvailableVfsMode() : Vfs::Off; + + const auto folderMan = FolderMan::instance(); + + definition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles(); + definition.alias = folderMan->map().size() > 0 ? QString::number(folderMan->map().size()) : QString::number(0); + + if (folderMan->navigationPaneHelper().showInExplorerNavigationPane()) { + definition.navigationPaneClsid = QUuid::createUuid(); + } + + folderMan->setSyncEnabled(false); + + if (const auto folder = folderMan->addFolder(accountState, definition)) { + if (definition.virtualFilesMode != Vfs::Off) { + folder->setRootPinState(PinState::OnlineOnly); + } + folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, QStringList() << QLatin1String("/")); + qCInfo(lcAccountSetupCommandLineJob) << QStringLiteral("Folder %1 setup from command line success.").arg(definition.localPath); + printAccountSetupFromCommandLineStatusAndExit(QStringLiteral("Account %1 setup from command line success.").arg(_account->displayName()), false); + } else { + AccountManager::instance()->deleteAccount(accountState); + printAccountSetupFromCommandLineStatusAndExit( + QStringLiteral("Account %1 setup from command line failed, due to folder creation failure.").arg(_account->displayName()), + false); + } +} + +void AccountSetupFromCommandLineJob::printAccountSetupFromCommandLineStatusAndExit(const QString &status, bool isFailure) +{ + if (isFailure) { + qCWarning(lcAccountSetupCommandLineJob) << status; + } else { + qCInfo(lcAccountSetupCommandLineJob) << status; + } + QTimer::singleShot(0, this, [this, isFailure]() { + this->deleteLater(); + if (!isFailure) { + qApp->quit(); + } else { + qApp->exit(1); + } + }); +} + +void AccountSetupFromCommandLineJob::fetchUserName() +{ + const auto fetchUserNameJob = new JsonApiJob(_account, QStringLiteral("/ocs/v1.php/cloud/user")); + connect(fetchUserNameJob, &JsonApiJob::jsonReceived, this, [this](const QJsonDocument &json, int statusCode) { + sender()->deleteLater(); + + if (statusCode != 100) { + printAccountSetupFromCommandLineStatusAndExit("Could not fetch username.", true); + return; + } + + const auto objData = json.object().value("ocs").toObject().value("data").toObject(); + const auto userId = objData.value("id").toString(""); + const auto displayName = objData.value("display-name").toString(""); + _account->setDavUser(userId); + _account->setDavDisplayName(displayName); + + checkLastModifiedWithPropfind(); + }); + fetchUserNameJob->start(); +} +} diff --git a/src/gui/accountsetupfromcommandlinejob.h b/src/gui/accountsetupfromcommandlinejob.h new file mode 100644 index 000000000..8485c72a1 --- /dev/null +++ b/src/gui/accountsetupfromcommandlinejob.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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. + */ + +#pragma once + +#include "account.h" + +#include +#include +#include + +namespace OCC +{ +class AccountState; + +class AccountSetupFromCommandLineJob : public QObject +{ + Q_OBJECT + +public: + AccountSetupFromCommandLineJob(QString appPassword, + QString userId, + QUrl serverUrl, + QString localDirPath = {}, + bool nonVfsMode = false, + QString remoteDirPath = QStringLiteral("/"), + QObject *parent = nullptr); + +public slots: + void handleAccountSetupFromCommandLine(); + +private slots: + void checkLastModifiedWithPropfind(); + + void accountSetupFromCommandLinePropfindHandleSuccess(); + + void accountSetupFromCommandLinePropfindHandleFailure(); + + void setupLocalSyncFolder(AccountState *accountState); + + void printAccountSetupFromCommandLineStatusAndExit(const QString &status, bool isFailure); + + void fetchUserName(); + +private: + QString _appPassword; + QString _userId; + QUrl _serverUrl; + QString _localDirPath; + bool _isVfsEnabled = true; + QString _remoteDirPath; + + AccountPtr _account; +}; +} diff --git a/src/gui/application.cpp b/src/gui/application.cpp index b75ebb121..8af99a7df 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -21,6 +21,7 @@ #include "config.h" #include "account.h" +#include "accountsetupcommandlinemanager.h" #include "accountstate.h" #include "editlocallymanager.h" #include "connectionvalidator.h" @@ -85,7 +86,13 @@ namespace { " --logflush : flush the log file after every write.\n" " --logdebug : also output debug-level messages in the log.\n" " --confdir : Use the given configuration folder.\n" - " --background : launch the application in the background.\n"; + " --background : launch the application in the background.\n" + " --userid : userId (username as on the server) to pass when creating an account via command-line.\n" + " --apppassword : appPassword to pass when creating an account via command-line.\n" + " --localdirpath : (optional) path where to create a local sync folder when creating an account via command-line.\n" + " --isvfsenabled : whether to set a VFS or non-VFS folder (1 for 'yes' or 0 for 'no') when creating an account via command-line.\n" + " --remotedirpath : (optional) path to a remote subfolder when creating an account via command-line.\n" + " --serverurl : a server URL to use when creating an account via command-line.\n"; QString applicationTrPath() { @@ -411,6 +418,11 @@ Application::Application(int &argc, char **argv) _gui->createTray(); handleEditLocallyFromOptions(); + + if (AccountSetupCommandLineManager::instance()->isCommandLineParsed()) { + AccountSetupCommandLineManager::instance()->setupAccountFromCommandLine(); + } + AccountSetupCommandLineManager::destroy(); } Application::~Application() @@ -579,6 +591,11 @@ void Application::slotParseMessage(const QString &msg, QObject *) handleEditLocallyFromOptions(); + if (AccountSetupCommandLineManager::instance()->isCommandLineParsed()) { + AccountSetupCommandLineManager::instance()->setupAccountFromCommandLine(); + } + AccountSetupCommandLineManager::destroy(); + } else if (msg.startsWith(QLatin1String("MSG_SHOWMAINDIALOG"))) { qCInfo(lcApplication) << "Running for" << _startedAt.elapsed() / 1000.0 << "sec"; if (_startedAt.elapsed() < 10 * 1000) { @@ -665,7 +682,14 @@ void Application::parseOptions(const QStringList &options) } } else { - showHint("Unrecognized option '" + option.toStdString() + "'"); + QString errorMessage; + if (!AccountSetupCommandLineManager::instance()->parseCommandlineOption(option, it, errorMessage)) { + if (!errorMessage.isEmpty()) { + showHint(errorMessage.toStdString()); + return; + } + showHint("Unrecognized option '" + option.toStdString() + "'"); + } } } }