diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index a137916bf..3a845531e 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -81,6 +81,10 @@ set(client_SRCS conflictsolver.cpp connectionvalidator.h connectionvalidator.cpp + editlocallyjob.h + editlocallyjob.cpp + editlocallymanager.h + editlocallymanager.cpp folder.h folder.cpp foldercreationdialog.h diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index cce3621e7..331db6297 100644 --- a/src/gui/accountmanager.cpp +++ b/src/gui/accountmanager.cpp @@ -369,6 +369,21 @@ AccountStatePtr AccountManager::account(const QString &name) return it != _accounts.cend() ? *it : AccountStatePtr(); } +AccountStatePtr AccountManager::accountFromUserId(const QString &id) const +{ + for (const auto &account : accounts()) { + const auto isUserIdWithPort = id.split(QLatin1Char(':')).size() > 1; + const auto port = isUserIdWithPort ? account->account()->url().port() : -1; + const auto portString = (port > 0 && port != 80 && port != 443) ? QStringLiteral(":%1").arg(port) : QStringLiteral(""); + const QString davUserId = QStringLiteral("%1@%2").arg(account->account()->davUser(), account->account()->url().host()) + portString; + + if (davUserId == id) { + return account; + } + } + return {}; +} + AccountState *AccountManager::addAccount(const AccountPtr &newAccount) { auto id = newAccount->id(); diff --git a/src/gui/accountmanager.h b/src/gui/accountmanager.h index 2440f91a9..04ce8e57b 100644 --- a/src/gui/accountmanager.h +++ b/src/gui/accountmanager.h @@ -65,6 +65,12 @@ public: */ AccountStatePtr account(const QString &name); + /** + * Return the account state pointer for an account from its id + */ + + [[nodiscard]] AccountStatePtr accountFromUserId(const QString &id) const; + /** * Delete the AccountState */ diff --git a/src/gui/application.cpp b/src/gui/application.cpp index a04ee2f13..fafc33a24 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 "editlocallymanager.h" #include "connectionvalidator.h" #include "folder.h" #include "folderman.h" @@ -749,34 +750,10 @@ void Application::handleEditLocallyFromOptions() return; } - handleEditLocally(_editFileLocallyUrl); + EditLocallyManager::instance()->editLocally(_editFileLocallyUrl); _editFileLocallyUrl.clear(); } -void Application::handleEditLocally(const QUrl &url) const -{ - auto pathSplit = url.path().split('/', Qt::SkipEmptyParts); - - if (pathSplit.size() < 2) { - qCWarning(lcApplication) << "Invalid URL for file local editing: " + pathSplit.join('/'); - return; - } - - // for a sample URL "nc://open/admin@nextcloud.lan:8080/Photos/lovely.jpg", QUrl::path would return "admin@nextcloud.lan:8080/Photos/lovely.jpg" - const auto userId = pathSplit.takeFirst(); - const auto fileRemotePath = pathSplit.join('/'); - const auto urlQuery = QUrlQuery{url}; - - auto token = QString{}; - if (urlQuery.hasQueryItem(QStringLiteral("token"))) { - token = urlQuery.queryItemValue(QStringLiteral("token")); - } else { - qCWarning(lcApplication) << "Invalid URL for file local editing: missing token"; - } - - FolderMan::instance()->editFileLocally(userId, fileRemotePath, token); -} - QString substLang(const QString &lang) { // Map the more appropriate script codes @@ -917,7 +894,7 @@ bool Application::event(QEvent *event) // On macOS, Qt does not handle receiving a custom URI as it does on other systems (as an application argument). // Instead, it sends out a QFileOpenEvent. We therefore need custom handling for our URI handling on macOS. qCInfo(lcApplication) << "macOS: Opening local file for editing: " << openEvent->url(); - handleEditLocally(openEvent->url()); + EditLocallyManager::instance()->editLocally(openEvent->url()); } else { const auto errorParsingLocalFileEditingUrl = QStringLiteral("The supplied url for local file editing '%1' is invalid!").arg(openEvent->url().toString()); qCInfo(lcApplication) << errorParsingLocalFileEditingUrl; diff --git a/src/gui/application.h b/src/gui/application.h index 6c00f6a50..a485eea89 100644 --- a/src/gui/application.h +++ b/src/gui/application.h @@ -87,8 +87,6 @@ public slots: /// Attempt to show() the tray icon again. Used if no systray was available initially. void tryTrayAgain(); - void handleEditLocally(const QUrl &url) const; - protected: void parseOptions(const QStringList &); void setupTranslations(); diff --git a/src/gui/editlocallyjob.cpp b/src/gui/editlocallyjob.cpp new file mode 100644 index 000000000..63f9515f8 --- /dev/null +++ b/src/gui/editlocallyjob.cpp @@ -0,0 +1,322 @@ +/* + * 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 "editlocallyjob.h" + +#include +#include +#include + +#include "editlocallymanager.h" +#include "folder.h" +#include "folderman.h" +#include "syncengine.h" +#include "systray.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcEditLocallyJob, "nextcloud.gui.editlocallyjob", QtInfoMsg) + +EditLocallyJob::EditLocallyJob(const QString &userId, + const QString &relPath, + const QString &token, + QObject *parent) + : QObject{parent} + , _userId(userId) + , _relPath(relPath) + , _token(token) +{ +} + +void EditLocallyJob::startSetup() +{ + if (_token.isEmpty() || _relPath.isEmpty() || _userId.isEmpty()) { + qCWarning(lcEditLocallyJob) << "Could not start setup." + << "token:" << _token + << "relPath:" << _relPath + << "userId" << _userId; + return; + } + + // Show the loading dialog but don't show the filename until we have + // verified the token + Systray::instance()->createEditFileLocallyLoadingDialog({}); + + // We check the input data locally first, without modifying any state or + // showing any potentially misleading data to the user + if (!isTokenValid(_token)) { + qCWarning(lcEditLocallyJob) << "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(lcEditLocallyJob) << "Provided relPath was:" << _relPath << "which is not canonical."; + showError(tr("Invalid file path was provided."), tr("Please try again.")); + return; + } + + _accountState = AccountManager::instance()->accountFromUserId(_userId); + + if (!_accountState) { + qCWarning(lcEditLocallyJob) << "Could not find an account " << _userId << " to edit file " << _relPath << " locally."; + showError(tr("Could not find an account for local editing."), tr("Please try again.")); + return; + } + + // We now ask the server to verify the token, before we again modify any + // state or look at local files + startTokenRemoteCheck(); +} + +void EditLocallyJob::startTokenRemoteCheck() +{ + if (!_accountState || _relPath.isEmpty() || _token.isEmpty()) { + qCWarning(lcEditLocallyJob) << "Could not start token check." + << "accountState:" << _accountState + << "relPath:" << _relPath + << "token:" << _token; + return; + } + + const auto encodedToken = QString::fromUtf8(QUrl::toPercentEncoding(_token)); // Sanitise the token + const auto encodedRelPath = QUrl::toPercentEncoding(_relPath); // Sanitise the relPath + + _checkTokenJob.reset(new SimpleApiJob(_accountState->account(), + QStringLiteral("/ocs/v2.php/apps/files/api/v1/openlocaleditor/%1").arg(encodedToken))); + + QUrlQuery params; + params.addQueryItem(QStringLiteral("path"), prefixSlashToPath(encodedRelPath)); + _checkTokenJob->addQueryParams(params); + _checkTokenJob->setVerb(SimpleApiJob::Verb::Post); + connect(_checkTokenJob.get(), &SimpleApiJob::resultReceived, this, &EditLocallyJob::remoteTokenCheckResultReceived); + + _checkTokenJob->start(); +} + +void EditLocallyJob::remoteTokenCheckResultReceived(const int statusCode) +{ + qCInfo(lcEditLocallyJob) << "token check result" << statusCode; + + constexpr auto HTTP_OK_CODE = 200; + _tokenVerified = statusCode == HTTP_OK_CODE; + + if (!_tokenVerified) { + showError(tr("Could not validate the request to open a file from server."), tr("Please try again.")); + return; + } + + proceedWithSetup(); +} + +void EditLocallyJob::proceedWithSetup() +{ + if (!_tokenVerified) { + qCWarning(lcEditLocallyJob) << "Could not proceed with setup as token is not verified."; + 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.isEmpty()) { + 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(); + + Systray::instance()->destroyEditFileLocallyLoadingDialog(); + Q_EMIT setupFinished(); +} + +QString EditLocallyJob::prefixSlashToPath(const QString &path) +{ + return path.startsWith('/') ? path : QChar::fromLatin1('/') + path; +} + +bool EditLocallyJob::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); + + return regexMatch.hasMatch(); +} + +bool EditLocallyJob::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 EditLocallyJob::isRelPathExcluded(const QString &relPath) +{ + if (relPath.isEmpty()) { + return false; + } + + 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 true; + } + } + } + + return false; +} + +void EditLocallyJob::showError(const QString &message, const QString &informativeText) +{ + Systray::instance()->destroyEditFileLocallyLoadingDialog(); + showErrorNotification(message, informativeText); + // to make sure the error is not missed, show a message box in addition + showErrorMessageBox(message, informativeText); + Q_EMIT error(message, informativeText); +} + +void EditLocallyJob::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 EditLocallyJob::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 EditLocallyJob::startEditLocally() +{ + if (_fileName.isEmpty() || _localFilePath.isEmpty() || !_folderForFile) { + qCWarning(lcEditLocallyJob) << "Could not start to edit locally." + << "fileName:" << _fileName + << "localFilePath:" << _localFilePath + << "folderForFile:" << _folderForFile; + return; + } + + Systray::instance()->createEditFileLocallyLoadingDialog(_fileName); + + _folderForFile->startSync(); + const auto syncFinishedConnection = connect(_folderForFile, &Folder::syncFinished, + this, &EditLocallyJob::folderSyncFinished); + + EditLocallyManager::instance()->folderSyncFinishedConnections.insert(_localFilePath, + syncFinishedConnection); +} + +void EditLocallyJob::folderSyncFinished(const OCC::SyncResult &result) +{ + Q_UNUSED(result) + disconnectSyncFinished(); + openFile(); +} + +void EditLocallyJob::disconnectSyncFinished() const +{ + if(_localFilePath.isEmpty()) { + return; + } + + const auto manager = EditLocallyManager::instance(); + + if (const auto existingConnection = manager->folderSyncFinishedConnections.value(_localFilePath)) { + disconnect(existingConnection); + manager->folderSyncFinishedConnections.remove(_localFilePath); + } +} + +void EditLocallyJob::openFile() +{ + if(_localFilePath.isEmpty()) { + qCWarning(lcEditLocallyJob) << "Could not edit locally. Invalid local file path."; + return; + } + + 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 fileOpened(); +} + +} diff --git a/src/gui/editlocallyjob.h b/src/gui/editlocallyjob.h new file mode 100644 index 000000000..258382a80 --- /dev/null +++ b/src/gui/editlocallyjob.h @@ -0,0 +1,81 @@ +/* + * 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 "accountstate.h" + +namespace OCC { + +class EditLocallyJob; +using EditLocallyJobPtr = QSharedPointer; + +class Folder; +class SyncResult; + +class EditLocallyJob : public QObject +{ + Q_OBJECT + +public: + explicit EditLocallyJob(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); + +signals: + void setupFinished(); + void error(const QString &message, const QString &informativeText); + void fileOpened(); + +public slots: + void startSetup(); + void startEditLocally(); + +private slots: + void startTokenRemoteCheck(); + void proceedWithSetup(); + + void showError(const QString &message, const QString &informativeText); + void showErrorNotification(const QString &message, const QString &informativeText) const; + void showErrorMessageBox(const QString &message, const QString &informativeText) const; + + void remoteTokenCheckResultReceived(const int statusCode); + void folderSyncFinished(const OCC::SyncResult &result); + + void disconnectSyncFinished() const; + void openFile(); + +private: + bool _tokenVerified = false; + + AccountStatePtr _accountState; + QString _userId; + QString _relPath; + QString _token; + + QString _fileName; + QString _localFilePath; + Folder *_folderForFile = nullptr; + std::unique_ptr _checkTokenJob; +}; + +} diff --git a/src/gui/editlocallymanager.cpp b/src/gui/editlocallymanager.cpp new file mode 100644 index 000000000..567a4abbd --- /dev/null +++ b/src/gui/editlocallymanager.cpp @@ -0,0 +1,91 @@ +/* + * 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 "editlocallymanager.h" + +#include +#include + +namespace OCC { + +Q_LOGGING_CATEGORY(lcEditLocallyManager, "nextcloud.gui.editlocallymanager", QtInfoMsg) + +EditLocallyManager *EditLocallyManager::_instance = nullptr; + +EditLocallyManager::EditLocallyManager(QObject *parent) + : QObject{parent} +{ +} + +EditLocallyManager *EditLocallyManager::instance() +{ + if (!_instance) { + _instance = new EditLocallyManager(); + } + return _instance; +} + +void EditLocallyManager::editLocally(const QUrl &url) +{ + const auto inputs = parseEditLocallyUrl(url); + createJob(inputs.userId, inputs.relPath, inputs.token); +} + +EditLocallyManager::EditLocallyInputData EditLocallyManager::parseEditLocallyUrl(const QUrl &url) +{ + const auto separator = QChar::fromLatin1('/'); + auto pathSplit = url.path().split(separator, Qt::SkipEmptyParts); + + if (pathSplit.size() < 2) { + qCWarning(lcEditLocallyManager) << "Invalid URL for file local editing: " + pathSplit.join(separator); + return {}; + } + + // for a sample URL "nc://open/admin@nextcloud.lan:8080/Photos/lovely.jpg", QUrl::path would return "admin@nextcloud.lan:8080/Photos/lovely.jpg" + const auto userId = pathSplit.takeFirst(); + const auto fileRemotePath = pathSplit.join(separator); + const auto urlQuery = QUrlQuery{url}; + + auto token = QString{}; + if (urlQuery.hasQueryItem(QStringLiteral("token"))) { + token = urlQuery.queryItemValue(QStringLiteral("token")); + } else { + qCWarning(lcEditLocallyManager) << "Invalid URL for file local editing: missing token"; + } + + return {userId, fileRemotePath, token}; +} + +void EditLocallyManager::createJob(const QString &userId, + const QString &relPath, + const QString &token) +{ + const EditLocallyJobPtr job(new EditLocallyJob(userId, relPath, token)); + // We need to make sure the job sticks around until it is finished + _jobs.insert(token, job); + + const auto removeJob = [this, token] { _jobs.remove(token); }; + const auto setupJob = [job] { job->startEditLocally(); }; + + connect(job.data(), &EditLocallyJob::error, + this, removeJob); + connect(job.data(), &EditLocallyJob::fileOpened, + this, removeJob); + connect(job.data(), &EditLocallyJob::setupFinished, + job.data(), setupJob); + + job->startSetup(); +} + +} diff --git a/src/gui/editlocallymanager.h b/src/gui/editlocallymanager.h new file mode 100644 index 000000000..cf42f1ed7 --- /dev/null +++ b/src/gui/editlocallymanager.h @@ -0,0 +1,56 @@ +/* + * 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 + +#include "editlocallyjob.h" + +namespace OCC { + +class EditLocallyManager : public QObject +{ + Q_OBJECT + +public: + [[nodiscard]] static EditLocallyManager *instance(); + + QHash folderSyncFinishedConnections; + +public slots: + void editLocally(const QUrl &url); + +private slots: + void createJob(const QString &userId, + const QString &relPath, + const QString &token); + +private: + explicit EditLocallyManager(QObject *parent = nullptr); + static EditLocallyManager *_instance; + + struct EditLocallyInputData { + QString userId; + QString relPath; + QString token; + }; + + [[nodiscard]] static EditLocallyInputData parseEditLocallyUrl(const QUrl &url); + + QHash _jobs; +}; + +} diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 44bc904d6..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,161 +1435,6 @@ void FolderMan::setDirtyNetworkLimits() } } -void FolderMan::editFileLocally(const QString &userId, const QString &relPath, const QString &token) -{ - const auto showError = [this](const OCC::AccountStatePtr accountState, const QString &errorMessage, const QString &subject) { - 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, errorMessage, subject); - } - } - - // to make sure the error is not missed, show a message box in addition - const auto messageBox = new QMessageBox; - messageBox->setAttribute(Qt::WA_DeleteOnClose); - messageBox->setText(errorMessage); - messageBox->setInformativeText(subject); - messageBox->setIcon(QMessageBox::Warning); - messageBox->addButton(QMessageBox::StandardButton::Ok); - messageBox->show(); - messageBox->activateWindow(); - messageBox->raise(); - }; - - if (token.isEmpty()) { - qCWarning(lcFolderMan) << "Edit locally request is missing a valid token. Impossible to open the file."; - showError({}, tr("Edit locally request is not valid. Opening the file is forbidden."), userId); - return; - } - - const auto accountFound = [&userId]() { - for (const auto &account : AccountManager::instance()->accounts()) { - const auto isUserIdWithPort = userId.split(QLatin1Char(':')).size() > 1; - const auto port = isUserIdWithPort ? account->account()->url().port() : -1; - const auto portString = (port > 0 && port != 80 && port != 443) ? QStringLiteral(":%1").arg(port) : QStringLiteral(""); - const QString davUserId = QStringLiteral("%1@%2").arg(account->account()->davUser(), account->account()->url().host()) + portString; - - if (davUserId == userId) { - return account; - } - } - return AccountStatePtr{}; - }(); - - if (!accountFound) { - qCWarning(lcFolderMan) << "Could not find an account " << userId << " to edit file " << relPath << " locally."; - showError(accountFound, tr("Could not find an account for local editing"), userId); - return; - } - - // 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) { - qCWarning(lcFolderMan) << "Provided relPath was:" << relPath - << "which is not canonical (cleaned path was:" << cleanedPath << ")"; - showError(accountFound, tr("Invalid file path was provided."), tr("Please try again.")); - return; - } - - const auto foundFiles = findFileInLocalFolders(relPath, accountFound->account()); - - if (foundFiles.isEmpty()) { - 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)) { - showError(accountFound, tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), relPath); - return; - } - } - } - - showError(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) { - showError(accountFound, tr("Could not find a folder to sync."), relPath); - return; - } - - // 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()) { - showError(accountFound, tr("Invalid token received."), tr("Please try again.")); - return; - } - - const auto relPathSplit = relPath.split(QLatin1Char('/')); - if (relPathSplit.size() > 0) { - Systray::instance()->createEditFileLocallyLoadingDialog(relPathSplit.last()); - } else { - showError(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 encodedToken = QString::fromUtf8(QUrl::toPercentEncoding(token)); // Sanitise the token - const auto encodedRelPath = QUrl::toPercentEncoding(slashPrefixedPath); // Sanitise the relPath - const auto checkTokenForEditLocally = new SimpleApiJob(accountFound->account(), QStringLiteral("/ocs/v2.php/apps/files/api/v1/openlocaleditor/%1").arg(encodedToken)); - - QUrlQuery params; - params.addQueryItem(QStringLiteral("path"), slashPrefixedPath); - checkTokenForEditLocally->addQueryParams(params); - checkTokenForEditLocally->setVerb(SimpleApiJob::Verb::Post); - connect(checkTokenForEditLocally, &SimpleApiJob::resultReceived, checkTokenForEditLocally, [this, folderForFile, localFilePath, showError, accountFound, relPath] (int statusCode) { - constexpr auto HTTP_OK_CODE = 200; - if (statusCode != HTTP_OK_CODE) { - Systray::instance()->destroyEditFileLocallyLoadingDialog(); - showError(accountFound, tr("Could not validate the request to open a file from server."), relPath); - qCInfo(lcFolderMan()) << "token check result" << statusCode; - return; - } - - folderForFile->startSync(); - _localFileEditingSyncFinishedConnections.insert(localFilePath, QObject::connect(folderForFile, &Folder::syncFinished, this, - [this, localFilePath](const OCC::SyncResult &result) { - Q_UNUSED(result); - const auto foundConnectionIt = _localFileEditingSyncFinishedConnections.find(localFilePath); - if (foundConnectionIt != std::end(_localFileEditingSyncFinishedConnections) && foundConnectionIt.value()) { - QObject::disconnect(foundConnectionIt.value()); - _localFileEditingSyncFinishedConnections.erase(foundConnectionIt); - } - // 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(); - }); - })); - }); - checkTokenForEditLocally->start(); -} - void FolderMan::trayOverallStatus(const QList &folders, SyncResult::Status *status, bool *unresolvedConflicts) { diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 55db5baba..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. @@ -377,8 +374,6 @@ private: bool _appRestartRequired = false; - QMap _localFileEditingSyncFinishedConnections; - static FolderMan *_instance; explicit FolderMan(QObject *parent = nullptr); friend class OCC::Application; diff --git a/src/gui/tray/EditFileLocallyLoadingDialog.qml b/src/gui/tray/EditFileLocallyLoadingDialog.qml index f7fbe965b..12e8f757b 100644 --- a/src/gui/tray/EditFileLocallyLoadingDialog.qml +++ b/src/gui/tray/EditFileLocallyLoadingDialog.qml @@ -65,13 +65,14 @@ Window { font.pixelSize: root.fontPixelSize color: Style.ncTextColor horizontalAlignment: Text.AlignHCenter + visible: root.fileName !== "" } Label { id: labelMessage Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true Layout.bottomMargin: Style.standardSpacing - text: qsTr("Opening for local editing") + text: qsTr("Opening file for local editing") elide: Text.ElideRight font.pixelSize: root.fontPixelSize color: Style.ncTextColor