conflictsolver.cpp
connectionvalidator.h
connectionvalidator.cpp
+ editlocallyhandler.h
+ editlocallyhandler.cpp
folder.h
folder.cpp
foldercreationdialog.h
#include "config.h"
#include "account.h"
#include "accountstate.h"
+#include "editlocallyhandler.h"
#include "connectionvalidator.h"
#include "folder.h"
#include "folderman.h"
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)
--- /dev/null
+/*
+ * 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 "editlocallyhandler.h"
+
+#include <QMessageBox>
+#include <QDesktopServices>
+#include <QtConcurrent>
+
+#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<QString, QMetaObject::Connection> 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();
+}
+
+}
--- /dev/null
+/*
+ * 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 "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;
+};
+
+}
#include <QMutableSetIterator>
#include <QSet>
#include <QNetworkProxy>
-#include <QDesktopServices>
-#include <QtConcurrent>
static const char versionC[] = "version";
static const int maxFoldersVersion = 1;
}
}
-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<Folder *> &folders,
SyncResult::Status *status, bool *unresolvedConflicts)
{
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.
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.
[[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<Folder *> _disabledFolders;
Folder::Map _folderMap;
QString _folderConfigPath;
bool _appRestartRequired = false;
- QHash<QString, QMetaObject::Connection> _editLocallySyncFinishedConnections;
-
static FolderMan *_instance;
explicit FolderMan(QObject *parent = nullptr);
friend class OCC::Application;