From: Claudio Cambra Date: Wed, 26 Oct 2022 18:13:38 +0000 (+0200) Subject: Refactor edit locally into its own class, rather than bolting onto FolderMan X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~12^2~11^2~177^2~6 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=9a00aa6e66a40d73ac8df45f92891f5cf8dec3d2;p=nextcloud-desktop.git Refactor edit locally into its own class, rather than bolting onto FolderMan Signed-off-by: Claudio Cambra --- diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index a137916bf..6aa35f6b6 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -81,6 +81,8 @@ set(client_SRCS conflictsolver.cpp connectionvalidator.h connectionvalidator.cpp + editlocallyhandler.h + editlocallyhandler.cpp folder.h folder.cpp foldercreationdialog.h diff --git a/src/gui/application.cpp b/src/gui/application.cpp index a04ee2f13..8538a59b0 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -22,6 +22,7 @@ #include "config.h" #include "account.h" #include "accountstate.h" +#include "editlocallyhandler.h" #include "connectionvalidator.h" #include "folder.h" #include "folderman.h" @@ -774,7 +775,14 @@ void Application::handleEditLocally(const QUrl &url) const qCWarning(lcApplication) << "Invalid URL for file local editing: missing token"; } - FolderMan::instance()->editFileLocally(userId, fileRemotePath, token); + // We need to make sure the handler sticks around until it is finished + const auto editLocallyHandler = new EditLocallyHandler(userId, fileRemotePath, token); + if (editLocallyHandler->ready()) { + connect(editLocallyHandler, &EditLocallyHandler::finished, this, [&editLocallyHandler] { delete editLocallyHandler; }); + editLocallyHandler->startEditLocally(); + } else { + delete editLocallyHandler; + } } QString substLang(const QString &lang) diff --git a/src/gui/editlocallyhandler.cpp b/src/gui/editlocallyhandler.cpp new file mode 100644 index 000000000..3bacf816c --- /dev/null +++ b/src/gui/editlocallyhandler.cpp @@ -0,0 +1,271 @@ +/* + * Copyright (C) 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 "editlocallyhandler.h" + +#include +#include +#include + +#include "accountmanager.h" +#include "folder.h" +#include "folderman.h" +#include "syncengine.h" +#include "systray.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcEditLocallyHandler, "nextcloud.gui.editlocallyhandler", QtInfoMsg) + +static QHash editLocallySyncFinishedConnections; + +EditLocallyHandler::EditLocallyHandler(const QString &userId, + const QString &relPath, + const QString &token, + QObject *parent) + : QObject{parent} + , _relPath(relPath) + , _token(token) +{ + _accountState = AccountManager::instance()->accountFromUserId(userId); + + if (!_accountState) { + qCWarning(lcEditLocallyHandler) << "Could not find an account " << userId << " to edit file " << relPath << " locally."; + showError(tr("Could not find an account for local editing"), userId); + return; + } + + if (!isTokenValid(token)) { + qCWarning(lcEditLocallyHandler) << "Edit locally request is missing a valid token, will not open file. Token received was:" << token; + showError(tr("Invalid token received."), tr("Please try again.")); + return; + } + + if (!isRelPathValid(relPath)) { + qCWarning(lcEditLocallyHandler) << "Provided relPath was:" << relPath << "which is not canonical."; + showError(tr("Invalid file path was provided."), tr("Please try again.")); + return; + } + + const auto foundFiles = FolderMan::instance()->findFileInLocalFolders(relPath, _accountState->account()); + + if (foundFiles.isEmpty()) { + if (isRelPathExcluded(relPath)) { + showError(tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), relPath); + } else { + showError(tr("Could not find a file for local editing. Make sure its path is valid and it is synced locally."), relPath); + } + return; + } + + _localFilePath = foundFiles.first(); + _folderForFile = FolderMan::instance()->folderForPath(_localFilePath); + + if (!_folderForFile) { + showError(tr("Could not find a folder to sync."), relPath); + return; + } + + const auto relPathSplit = relPath.split(QLatin1Char('/')); + if (relPathSplit.size() == 0) { + showError(tr("Could not find a file for local editing. Make sure its path is valid and it is synced locally."), relPath); + return; + } + + _fileName = relPathSplit.last(); + + _ready = true; +} + +bool EditLocallyHandler::ready() const +{ + return _ready; +} + +QString EditLocallyHandler::prefixSlashToPath(const QString &path) +{ + auto slashPrefixedPath = path; + if (!slashPrefixedPath.startsWith('/')) { + slashPrefixedPath.prepend('/'); + } + + return slashPrefixedPath; +} + +bool EditLocallyHandler::isTokenValid(const QString &token) +{ + if (token.isEmpty()) { + return false; + } + + // Token is an alphanumeric string 128 chars long. + // Ensure that is what we received and what we are sending to the server. + const QRegularExpression tokenRegex("^[a-zA-Z0-9]{128}$"); + const auto regexMatch = tokenRegex.match(token); + + // Means invalid token type received, be cautious with bad token + if(!regexMatch.hasMatch()) { + return false; + } + + return true; +} + +bool EditLocallyHandler::isRelPathValid(const QString &relPath) +{ + if (relPath.isEmpty()) { + return false; + } + + // We want to check that the path is canonical and not relative + // (i.e. that it doesn't contain ../../) but we always receive + // a relative path, so let's make it absolute by prepending a + // slash + const auto slashPrefixedPath = prefixSlashToPath(relPath); + + // Let's check that the filepath is canonical, and that the request + // contains no funny behaviour regarding paths + const auto cleanedPath = QDir::cleanPath(slashPrefixedPath); + + if (cleanedPath != slashPrefixedPath) { + return false; + } + + return true; +} + +bool EditLocallyHandler::isRelPathExcluded(const QString &relPath) +{ + if (relPath.isEmpty()) { + return true; + } + + const auto folderMap = FolderMan::instance()->map(); + for (const auto &folder : folderMap) { + bool result = false; + const auto excludedThroughSelectiveSync = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &result); + for (const auto &excludedPath : excludedThroughSelectiveSync) { + if (relPath.startsWith(excludedPath)) { + return false; + } + } + } + + return true; +} + +void EditLocallyHandler::showError(const QString &message, const QString &informativeText) const +{ + showErrorNotification(message, informativeText); + // to make sure the error is not missed, show a message box in addition + showErrorMessageBox(message, informativeText); +} + +void EditLocallyHandler::showErrorNotification(const QString &message, const QString &informativeText) const +{ + if (!_accountState || !_accountState->account()) { + return; + } + + const auto folderMap = FolderMan::instance()->map(); + const auto foundFolder = std::find_if(folderMap.cbegin(), folderMap.cend(), [this](const auto &folder) { + return _accountState->account()->davUrl() == folder->remoteUrl(); + }); + + if (foundFolder != folderMap.cend()) { + (*foundFolder)->syncEngine().addErrorToGui(SyncFileItem::SoftError, message, informativeText); + } +} + +void EditLocallyHandler::showErrorMessageBox(const QString &message, const QString &informativeText) const +{ + const auto messageBox = new QMessageBox; + messageBox->setAttribute(Qt::WA_DeleteOnClose); + messageBox->setText(message); + messageBox->setInformativeText(informativeText); + messageBox->setIcon(QMessageBox::Warning); + messageBox->addButton(QMessageBox::StandardButton::Ok); + messageBox->show(); + messageBox->activateWindow(); + messageBox->raise(); +} + +void EditLocallyHandler::startEditLocally() +{ + if (!_ready) { + return; + } + + Systray::instance()->createEditFileLocallyLoadingDialog(_fileName); + + const auto encodedToken = QString::fromUtf8(QUrl::toPercentEncoding(_token)); // Sanitise the token + const auto encodedRelPath = QUrl::toPercentEncoding(_relPath); // Sanitise the relPath + const auto checkEditLocallyToken = new SimpleApiJob(_accountState->account(), QStringLiteral("/ocs/v2.php/apps/files/api/v1/openlocaleditor/%1").arg(encodedToken)); + + QUrlQuery params; + params.addQueryItem(QStringLiteral("path"), prefixSlashToPath(_relPath)); + checkEditLocallyToken->addQueryParams(params); + checkEditLocallyToken->setVerb(SimpleApiJob::Verb::Post); + connect(checkEditLocallyToken, &SimpleApiJob::resultReceived, this, &EditLocallyHandler::remoteTokenCheckFinished); + + checkEditLocallyToken->start(); +} + +void EditLocallyHandler::remoteTokenCheckFinished(const int statusCode) +{ + constexpr auto HTTP_OK_CODE = 200; + if (statusCode != HTTP_OK_CODE) { + Systray::instance()->destroyEditFileLocallyLoadingDialog(); + + showError(tr("Could not validate the request to open a file from server."), _relPath); + qCInfo(lcEditLocallyHandler) << "token check result" << statusCode; + return; + } + + _folderForFile->startSync(); + const auto syncFinishedConnection = connect(_folderForFile, &Folder::syncFinished, + this, &EditLocallyHandler::folderSyncFinished); + editLocallySyncFinishedConnections.insert(_localFilePath, syncFinishedConnection); +} + +void EditLocallyHandler::folderSyncFinished(const OCC::SyncResult &result) +{ + Q_UNUSED(result) + disconnectSyncFinished(); + openFile(); +} + +void EditLocallyHandler::disconnectSyncFinished() const +{ + if (const auto existingConnection = editLocallySyncFinishedConnections.value(_localFilePath)) { + disconnect(existingConnection); + editLocallySyncFinishedConnections.remove(_localFilePath); + } +} + +void EditLocallyHandler::openFile() +{ + const auto localFilePath = _localFilePath; + // In case the VFS mode is enabled and a file is not yet hydrated, we must call QDesktopServices::openUrl + // from a separate thread, or, there will be a freeze. To avoid searching for a specific folder and checking + // if the VFS is enabled - we just always call it from a separate thread. + QtConcurrent::run([localFilePath]() { + QDesktopServices::openUrl(QUrl::fromLocalFile(localFilePath)); + Systray::instance()->destroyEditFileLocallyLoadingDialog(); + }); + + Q_EMIT finished(); +} + +} diff --git a/src/gui/editlocallyhandler.h b/src/gui/editlocallyhandler.h new file mode 100644 index 000000000..9d6f9677c --- /dev/null +++ b/src/gui/editlocallyhandler.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 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. + */ + +#pragma once + +#include + +#include "accountmanager.h" +#include "folder.h" + +namespace OCC { + +class EditLocallyHandler : public QObject +{ + Q_OBJECT + +public: + explicit EditLocallyHandler(const QString &userId, + const QString &relPath, + const QString &token, + QObject *parent = nullptr); + + [[nodiscard]] static bool isTokenValid(const QString &token); + [[nodiscard]] static bool isRelPathValid(const QString &relPath); + [[nodiscard]] static bool isRelPathExcluded(const QString &relPath); + [[nodiscard]] static QString prefixSlashToPath(const QString &path); + + [[nodiscard]] bool ready() const; + +signals: + void finished(); + +public slots: + void startEditLocally(); + void startTokenRemoteCheck(); + +private slots: + void showError(const QString &message, const QString &informativeText) const; + void showErrorNotification(const QString &message, const QString &informativeText) const; + void showErrorMessageBox(const QString &message, const QString &informativeText) const; + + void remoteTokenCheckFinished(const int statusCode); + void folderSyncFinished(const OCC::SyncResult &result); + + void disconnectSyncFinished() const; + void openFile(); + +private: + bool _ready = false; + + AccountStatePtr _accountState; + QString _relPath; + QString _token; + + QString _fileName; + QString _localFilePath; + Folder *_folderForFile = nullptr; +}; + +} diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 045c50e26..8d57a72b8 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -36,8 +36,6 @@ #include #include #include -#include -#include static const char versionC[] = "version"; static const int maxFoldersVersion = 1; @@ -1437,224 +1435,6 @@ void FolderMan::setDirtyNetworkLimits() } } -void FolderMan::editFileLocally(const QString &userId, const QString &relPath, const QString &token) -{ - const auto accountFound = AccountManager::instance()->accountFromUserId(userId); - - if (!accountFound) { - qCWarning(lcFolderMan) << "Could not find an account " << userId << " to edit file " << relPath << " locally."; - showEditLocallyError(accountFound, tr("Could not find an account for local editing"), userId); - return; - } - - if (!isEditLocallyTokenValid(token)) { - qCWarning(lcFolderMan) << "Edit locally request is missing a valid token, will not open file. Token received was:" << token; - showEditLocallyError(accountFound, tr("Invalid token received."), tr("Please try again.")); - return; - } - - if (!isEditLocallyRelPathValid(relPath)) { - qCWarning(lcFolderMan) << "Provided relPath was:" << relPath << "which is not canonical."; - showEditLocallyError(accountFound, tr("Invalid file path was provided."), tr("Please try again.")); - return; - } - - const auto foundFiles = findFileInLocalFolders(relPath, accountFound->account()); - - if (foundFiles.isEmpty()) { - if (isEditLocallyRelPathExcluded(relPath)) { - showEditLocallyError(accountFound, tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), relPath); - } else { - showEditLocallyError(accountFound, tr("Could not find a file for local editing. Make sure its path is valid and it is synced locally."), relPath); - } - return; - } - - const auto localFilePath = foundFiles.first(); - const auto folderForFile = folderForPath(localFilePath); - - if (!folderForFile) { - showEditLocallyError(accountFound, tr("Could not find a folder to sync."), relPath); - return; - } - - const auto relPathSplit = relPath.split(QLatin1Char('/')); - if (relPathSplit.size() == 0) { - showEditLocallyError(accountFound, tr("Could not find a file for local editing. Make sure its path is valid and it is synced locally."), relPath); - return; - } - - startEditLocally(accountFound, relPath, token, relPathSplit.last(), localFilePath, folderForFile); -} - -bool FolderMan::isEditLocallyTokenValid(const QString &token) const -{ - if (token.isEmpty()) { - return false; - } - - // Token is an alphanumeric string 128 chars long. - // Ensure that is what we received and what we are sending to the server. - const QRegularExpression tokenRegex("^[a-zA-Z0-9]{128}$"); - const auto regexMatch = tokenRegex.match(token); - - // Means invalid token type received, be cautious with bad token - if(!regexMatch.hasMatch()) { - return false; - } - - return true; -} - -bool FolderMan::isEditLocallyRelPathValid(const QString &relPath) const -{ - if (relPath.isEmpty()) { - return false; - } - - // We want to check that the path is canonical and not relative - // (i.e. that it doesn't contain ../../) but we always receive - // a relative path, so let's make it absolute by prepending a - // slash - - auto slashPrefixedPath = relPath; - if (!slashPrefixedPath.startsWith('/')) { - slashPrefixedPath.prepend('/'); - } - - // Let's check that the filepath is canonical, and that the request - // contains no funny behaviour regarding paths - const auto cleanedPath = QDir::cleanPath(slashPrefixedPath); - - if (cleanedPath != slashPrefixedPath) { - return false; - } - - return true; -} - -bool FolderMan::isEditLocallyRelPathExcluded(const QString &relPath) const -{ - if (relPath.isEmpty()) { - return true; - } - - for (const auto &folder : map()) { - bool result = false; - const auto excludedThroughSelectiveSync = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &result); - for (const auto &excludedPath : excludedThroughSelectiveSync) { - if (relPath.startsWith(excludedPath)) { - return false; - } - } - } - - return true; -} - -void FolderMan::showEditLocallyError(const AccountStatePtr &accountState, const QString &message, const QString &informativeText) const -{ - showEditLocallyErrorNotification(accountState, message, informativeText); - // to make sure the error is not missed, show a message box in addition - showEditLocallyErrorMessageBox(message, informativeText); -} - -void FolderMan::showEditLocallyErrorNotification(const AccountStatePtr &accountState, const QString &message, const QString &informativeText) const -{ - if (accountState && accountState->account()) { - const auto foundFolder = std::find_if(std::cbegin(map()), std::cend(map()), [accountState](const auto &folder) { - return accountState->account()->davUrl() == folder->remoteUrl(); - }); - - if (foundFolder != std::cend(map())) { - (*foundFolder)->syncEngine().addErrorToGui(SyncFileItem::SoftError, message, informativeText); - } - } -} - -void FolderMan::showEditLocallyErrorMessageBox(const QString &message, const QString &informativeText) const -{ - const auto messageBox = new QMessageBox; - messageBox->setAttribute(Qt::WA_DeleteOnClose); - messageBox->setText(message); - messageBox->setInformativeText(informativeText); - messageBox->setIcon(QMessageBox::Warning); - messageBox->addButton(QMessageBox::StandardButton::Ok); - messageBox->show(); - messageBox->activateWindow(); - messageBox->raise(); -} - -void FolderMan::startEditLocally(const AccountStatePtr &accountState, - const QString &relPath, - const QString &token, - const QString &fileName, - const QString &localFilePath, - Folder *folderForFile) -{ - Systray::instance()->createEditFileLocallyLoadingDialog(fileName); - - const auto encodedToken = QString::fromUtf8(QUrl::toPercentEncoding(token)); // Sanitise the token - const auto encodedRelPath = QUrl::toPercentEncoding(relPath); // Sanitise the relPath - const auto checkEditLocallyToken = new SimpleApiJob(accountState->account(), QStringLiteral("/ocs/v2.php/apps/files/api/v1/openlocaleditor/%1").arg(encodedToken)); - - QUrlQuery params; - params.addQueryItem(QStringLiteral("path"), QString{"/" + relPath}); - checkEditLocallyToken->addQueryParams(params); - - connect(checkEditLocallyToken, &SimpleApiJob::resultReceived, checkEditLocallyToken, [this, folderForFile, localFilePath, accountState, relPath] (int statusCode) { - editLocallyTokenCheckFinished(accountState, relPath, localFilePath, folderForFile, statusCode); - }); - checkEditLocallyToken->start(); -} - -void FolderMan::editLocallyTokenCheckFinished(const AccountStatePtr &accountState, - const QString &relPath, - const QString &localFilePath, - Folder *folderForFile, - const int statusCode) -{ - constexpr auto HTTP_OK_CODE = 200; - if (statusCode != HTTP_OK_CODE) { - Systray::instance()->destroyEditFileLocallyLoadingDialog(); - showEditLocallyError(accountState, tr("Could not validate the request to open a file from server."), relPath); - qCInfo(lcFolderMan()) << "token check result" << statusCode; - return; - } - - folderForFile->startSync(); - const auto syncFinishedConnection = connect(folderForFile, &Folder::syncFinished, this, [this, localFilePath](const OCC::SyncResult &result) { - Q_UNUSED(result); - editLocallyFolderSyncFinished(localFilePath); - }); - _editLocallySyncFinishedConnections.insert(localFilePath, syncFinishedConnection); -} - -void FolderMan::editLocallyFolderSyncFinished(const QString &localFilePath) -{ - disconnectEditLocallySyncFinishedConnections(localFilePath); - openEditLocallyFile(localFilePath); -} - -void FolderMan::disconnectEditLocallySyncFinishedConnections(const QString &localFilePath) -{ - if (const auto existingConnection = _editLocallySyncFinishedConnections.value(localFilePath)) { - disconnect(existingConnection); - _editLocallySyncFinishedConnections.remove(localFilePath); - } -} - -void FolderMan::openEditLocallyFile(const QString &localFilePath) const -{ - // In case the VFS mode is enabled and a file is not yet hydrated, we must call QDesktopServices::openUrl - // from a separate thread, or, there will be a freeze. To avoid searching for a specific folder and checking - // if the VFS is enabled - we just always call it from a separate thread. - QtConcurrent::run([localFilePath]() { - QDesktopServices::openUrl(QUrl::fromLocalFile(localFilePath)); - Systray::instance()->destroyEditFileLocallyLoadingDialog(); - }); -} - void FolderMan::trayOverallStatus(const QList &folders, SyncResult::Status *status, bool *unresolvedConflicts) { diff --git a/src/gui/folderman.h b/src/gui/folderman.h index e6322e90a..bf7a8e8c8 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -213,9 +213,6 @@ public: void setDirtyProxy(); void setDirtyNetworkLimits(); - /** opens a file with default app, if the file is present **/ - void editFileLocally(const QString &userId, const QString &relPath, const QString &token); - signals: /** * signal to indicate a folder has changed its sync state. @@ -311,27 +308,6 @@ private slots: void slotProcessFilesPushNotification(Account *account); void slotConnectToPushNotifications(Account *account); - void showEditLocallyError(const AccountStatePtr &accountState, const QString &message, const QString &informativeText) const; - void showEditLocallyErrorNotification(const AccountStatePtr &accountState, const QString &message, const QString &informativeText) const; - void showEditLocallyErrorMessageBox(const QString &message, const QString &informativeText) const; - - void startEditLocally(const AccountStatePtr &accountState, - const QString &relPath, - const QString &token, - const QString &fileName, - const QString &localFilePath, - Folder *folderForFile); - - void editLocallyTokenCheckFinished(const AccountStatePtr &accountState, - const QString &relPath, - const QString &localFilePath, - Folder *folderForFile, - const int statusCode); - - void disconnectEditLocallySyncFinishedConnections(const QString &localFilePath); - void editLocallyFolderSyncFinished(const QString &localFilePath); - void openEditLocallyFile(const QString &localFilePath) const; - private: /** Adds a new folder, does not add it to the account settings and * does not set an account on the new folder. @@ -364,11 +340,6 @@ private: [[nodiscard]] bool isSwitchToVfsNeeded(const FolderDefinition &folderDefinition) const; - [[nodiscard]] bool isEditLocallyTokenValid(const QString &token) const; - [[nodiscard]] bool isEditLocallyRelPathValid(const QString &relPath) const; - [[nodiscard]] bool isEditLocallyRelPathExcluded(const QString &relPath) const; - [[nodiscard]] bool editLocallyAccount(const AccountStatePtr &acountState) const; - QSet _disabledFolders; Folder::Map _folderMap; QString _folderConfigPath; @@ -403,8 +374,6 @@ private: bool _appRestartRequired = false; - QHash _editLocallySyncFinishedConnections; - static FolderMan *_instance; explicit FolderMan(QObject *parent = nullptr); friend class OCC::Application;