Merge pull request #5107 from nextcloud/bugfix/refactor-folderman-edit-locally

Refactor FolderMan's "Edit Locally" capabilities as separate class
This commit is contained in:
Claudio Cambra 2022-10-29 14:02:04 +02:00 committed by GitHub
commit 7ac4898840
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 580 additions and 191 deletions

View file

@ -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

View file

@ -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();

View file

@ -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
*/

View file

@ -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;

View file

@ -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();

322
src/gui/editlocallyjob.cpp Normal file
View file

@ -0,0 +1,322 @@
/*
* Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* 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 <QMessageBox>
#include <QDesktopServices>
#include <QtConcurrent>
#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();
}
}

81
src/gui/editlocallyjob.h Normal file
View file

@ -0,0 +1,81 @@
/*
* Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* 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 <QObject>
#include "accountstate.h"
namespace OCC {
class EditLocallyJob;
using EditLocallyJobPtr = QSharedPointer<EditLocallyJob>;
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<SimpleApiJob> _checkTokenJob;
};
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* 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 <QUrl>
#include <QLoggingCategory>
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();
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* 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 <QObject>
#include <QHash>
#include "editlocallyjob.h"
namespace OCC {
class EditLocallyManager : public QObject
{
Q_OBJECT
public:
[[nodiscard]] static EditLocallyManager *instance();
QHash<QString, QMetaObject::Connection> 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<QString, EditLocallyJobPtr> _jobs;
};
}

View file

@ -36,8 +36,6 @@
#include <QMutableSetIterator>
#include <QSet>
#include <QNetworkProxy>
#include <QDesktopServices>
#include <QtConcurrent>
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<Folder *> &folders,
SyncResult::Status *status, bool *unresolvedConflicts)
{

View file

@ -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<QString, QMetaObject::Connection> _localFileEditingSyncFinishedConnections;
static FolderMan *_instance;
explicit FolderMan(QObject *parent = nullptr);
friend class OCC::Application;

View file

@ -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