Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix/check token for edit locally requests #5039

Merged
merged 3 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/gui/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
#include <QMessageBox>
#include <QDesktopServices>
#include <QGuiApplication>
#include <QUrlQuery>

class QSocket;

Expand Down Expand Up @@ -764,8 +765,16 @@ void Application::handleEditLocally(const QUrl &url) const
// 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 accountDisplayName = pathSplit.takeFirst();
const auto fileRemotePath = pathSplit.join('/');
const auto urlQuery = QUrlQuery{url};

FolderMan::instance()->editFileLocally(accountDisplayName, fileRemotePath);
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(accountDisplayName, fileRemotePath, token);
}

QString substLang(const QString &lang)
Expand Down
55 changes: 38 additions & 17 deletions src/gui/folderman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1422,7 +1422,7 @@ void FolderMan::setDirtyNetworkLimits()
}
}

void FolderMan::editFileLocally(const QString &accountDisplayName, const QString &relPath)
void FolderMan::editFileLocally(const QString &accountDisplayName, const QString &relPath, const QString &token)
{
const auto showError = [this](const OCC::AccountStatePtr accountState, const QString &errorMessage, const QString &subject) {
if (accountState && accountState->account()) {
Expand All @@ -1447,6 +1447,12 @@ void FolderMan::editFileLocally(const QString &accountDisplayName, const QString
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."), accountDisplayName);
return;
}

const auto accountFound = AccountManager::instance()->account(accountDisplayName);

if (!accountFound) {
Expand Down Expand Up @@ -1488,23 +1494,38 @@ void FolderMan::editFileLocally(const QString &accountDisplayName, const QString
showError(accountFound, tr("Could not find a file for local editing. Make sure its path is valid and it is synced locally."), relPath);
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));

const auto checkTokenForEditLocally = new SimpleApiJob(accountFound->account(), QStringLiteral("/ocs/v2.php/apps/files/api/v1/openlocaleditor/%1").arg(token));
checkTokenForEditLocally->setVerb(SimpleApiJob::Verb::Post);
checkTokenForEditLocally->setBody(QByteArray{"path=/"}.append(relPath.toUtf8()));
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,
Expand Down
2 changes: 1 addition & 1 deletion src/gui/folderman.h
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ class FolderMan : public QObject
void setDirtyNetworkLimits();

/** opens a file with default app, if the file is present **/
void editFileLocally(const QString &accountDisplayName, const QString &relPath);
void editFileLocally(const QString &accountDisplayName, const QString &relPath, const QString &token);

signals:
/**
Expand Down
136 changes: 88 additions & 48 deletions src/libsync/networkjobs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Q_LOGGING_CATEGORY(lcAvatarJob, "nextcloud.sync.networkjob.avatar", QtInfoMsg)
Q_LOGGING_CATEGORY(lcMkColJob, "nextcloud.sync.networkjob.mkcol", QtInfoMsg)
Q_LOGGING_CATEGORY(lcProppatchJob, "nextcloud.sync.networkjob.proppatch", QtInfoMsg)
Q_LOGGING_CATEGORY(lcJsonApiJob, "nextcloud.sync.networkjob.jsonapi", QtInfoMsg)
Q_LOGGING_CATEGORY(lcSimpleApiJob, "nextcloud.sync.networkjob.simpleapi", QtInfoMsg)
Q_LOGGING_CATEGORY(lcDetermineAuthTypeJob, "nextcloud.sync.networkjob.determineauthtype", QtInfoMsg)
Q_LOGGING_CATEGORY(lcSimpleFileJob, "nextcloud.sync.networkjob.simplefilejob", QtInfoMsg)
const int notModifiedStatusCode = 304;
Expand Down Expand Up @@ -822,64 +823,23 @@ bool EntityExistsJob::finished()
/*********************************************************************************************/

JsonApiJob::JsonApiJob(const AccountPtr &account, const QString &path, QObject *parent)
: AbstractNetworkJob(account, path, parent)
{
}

void JsonApiJob::addQueryParams(const QUrlQuery &params)
{
_additionalParams = params;
}

void JsonApiJob::addRawHeader(const QByteArray &headerName, const QByteArray &value)
: SimpleApiJob(account, path, parent)
{
_request.setRawHeader(headerName, value);
}

void JsonApiJob::setBody(const QJsonDocument &body)
{
_body = body.toJson();
qCDebug(lcJsonApiJob) << "Set body for request:" << _body;
if (!_body.isEmpty()) {
_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
}
}


void JsonApiJob::setVerb(Verb value)
{
_verb = value;
}


QByteArray JsonApiJob::verbToString() const
{
switch (_verb) {
case Verb::Get:
return "GET";
case Verb::Post:
return "POST";
case Verb::Put:
return "PUT";
case Verb::Delete:
return "DELETE";
SimpleApiJob::setBody(body.toJson());
qCDebug(lcJsonApiJob) << "Set body for request:" << SimpleApiJob::body();
if (!SimpleApiJob::body().isEmpty()) {
request().setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
}
return "GET";
}

void JsonApiJob::start()
{
addRawHeader("OCS-APIREQUEST", "true");
auto query = _additionalParams;
query.addQueryItem(QLatin1String("format"), QLatin1String("json"));
QUrl url = Utility::concatUrlPath(account()->url(), path(), query);
const auto httpVerb = verbToString();
if (!_body.isEmpty()) {
sendRequest(httpVerb, url, _request, _body);
} else {
sendRequest(httpVerb, url, _request);
}
AbstractNetworkJob::start();
additionalParams().addQueryItem(QLatin1String("format"), QLatin1String("json"));
SimpleApiJob::start();
}

bool JsonApiJob::finished()
Expand Down Expand Up @@ -1183,4 +1143,84 @@ void fetchPrivateLinkUrl(AccountPtr account, const QString &remotePath,
job->start();
}

SimpleApiJob::SimpleApiJob(const AccountPtr &account, const QString &path, QObject *parent)
: AbstractNetworkJob(account, path, parent)
{
}

void SimpleApiJob::setBody(const QByteArray &body)
{
_body = body;
qCDebug(lcSimpleApiJob) << "Set body for request:" << _body;
}


void SimpleApiJob::setVerb(Verb value)
{
_verb = value;
}


QByteArray SimpleApiJob::verbToString() const
{
switch (_verb) {
case Verb::Get:
return "GET";
case Verb::Post:
return "POST";
case Verb::Put:
return "PUT";
case Verb::Delete:
return "DELETE";
}
return "GET";
}

void SimpleApiJob::start()
{
addRawHeader("OCS-APIREQUEST", "true");
auto query = _additionalParams;
QUrl url = Utility::concatUrlPath(account()->url(), path(), query);
const auto httpVerb = verbToString();
if (!SimpleApiJob::body().isEmpty()) {
sendRequest(httpVerb, url, request(), SimpleApiJob::body());
} else {
sendRequest(httpVerb, url, request());
}
AbstractNetworkJob::start();
}

bool SimpleApiJob::finished()
{
const auto httpStatusCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qCDebug(lcSimpleApiJob) << "result: " << path() << errorString() << httpStatusCode;
emit resultReceived(httpStatusCode);
return true;
}

QNetworkRequest& SimpleApiJob::request()
{
return _request;
}

QByteArray& SimpleApiJob::body()
{
return _body;
}

QUrlQuery &SimpleApiJob::additionalParams()
{
return _additionalParams;
}

void SimpleApiJob::addQueryParams(const QUrlQuery &params)
{
_additionalParams = params;
}

void SimpleApiJob::addRawHeader(const QByteArray &headerName, const QByteArray &value)
{
request().setRawHeader(headerName, value);
}

} // namespace OCC
79 changes: 50 additions & 29 deletions src/libsync/networkjobs.h
Original file line number Diff line number Diff line change
Expand Up @@ -382,22 +382,7 @@ private slots:
bool finished() override;
};

/**
* @brief Job to check an API that return JSON
*
* Note! you need to be in the connected state before calling this because of a server bug:
* https://github.com/owncloud/core/issues/12930
*
* To be used like this:
* \code
* _job = new JsonApiJob(account, QLatin1String("ocs/v1.php/foo/bar"), this);
* connect(job, SIGNAL(jsonReceived(QJsonDocument)), ...)
* The received QVariantMap is null in case of error
* \encode
*
* @ingroup libsync
*/
class OWNCLOUDSYNC_EXPORT JsonApiJob : public AbstractNetworkJob
class OWNCLOUDSYNC_EXPORT SimpleApiJob : public AbstractNetworkJob
{
Q_OBJECT
public:
Expand All @@ -406,9 +391,13 @@ class OWNCLOUDSYNC_EXPORT JsonApiJob : public AbstractNetworkJob
Post,
Put,
Delete,
};
};

explicit JsonApiJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr);
explicit SimpleApiJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr);

void setBody(const QByteArray &body);

void setVerb(Verb value);

/**
* @brief addQueryParams - add more parameters to the ocs call
Expand All @@ -423,9 +412,50 @@ class OWNCLOUDSYNC_EXPORT JsonApiJob : public AbstractNetworkJob
void addQueryParams(const QUrlQuery &params);
void addRawHeader(const QByteArray &headerName, const QByteArray &value);

void setBody(const QJsonDocument &body);
public slots:
void start() override;

void setVerb(Verb value);
Q_SIGNALS:

void resultReceived(int statusCode);

protected:
bool finished() override;

[[nodiscard]] QNetworkRequest& request();
[[nodiscard]] QByteArray& body();
[[nodiscard]] QUrlQuery& additionalParams();
[[nodiscard]] QByteArray verbToString() const;

private:
QByteArray _body;
QUrlQuery _additionalParams;
QNetworkRequest _request;
Verb _verb = Verb::Get;
};

/**
* @brief Job to check an API that return JSON
*
* Note! you need to be in the connected state before calling this because of a server bug:
* https://github.com/owncloud/core/issues/12930
*
* To be used like this:
* \code
* _job = new JsonApiJob(account, QLatin1String("ocs/v1.php/foo/bar"), this);
* connect(job, SIGNAL(jsonReceived(QJsonDocument)), ...)
* The received QVariantMap is null in case of error
* \encode
*
* @ingroup libsync
*/
class OWNCLOUDSYNC_EXPORT JsonApiJob : public SimpleApiJob
{
Q_OBJECT
public:
explicit JsonApiJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr);

void setBody(const QJsonDocument &body);

public slots:
void start() override;
Expand All @@ -448,15 +478,6 @@ public slots:
* @param statusCode - the OCS status code: 100 (!) for success
*/
void etagResponseHeaderReceived(const QByteArray &value, int statusCode);

private:
QByteArray _body;
QUrlQuery _additionalParams;
QNetworkRequest _request;

Verb _verb = Verb::Get;

[[nodiscard]] QByteArray verbToString() const;
};

/**
Expand Down