Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,153 @@
/*
* Copyright (C) Fabian Müller <fmueller@owncloud.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 "checkserverjobfactory.h"
#include "common/utility.h"
#include "libsync/accessmanager.h"
#include "libsync/account.h"
#include "libsync/cookiejar.h"
#include "libsync/creds/abstractcredentials.h"
#include "libsync/theme.h"
#include <QJsonParseError>
namespace {
// FIXME: this is not a permanent solution, eventually we want to replace the job factories with job classes so we can store such information there
class CheckServerCoreJob : OCC::CoreJob
{
Q_OBJECT
friend OCC::CheckServerJobFactory;
public:
using OCC::CoreJob::CoreJob;
private:
// doesn't concern users of the job factory
// we just need a place to maintain these variables, but the factory is likely deleted before the job has finished
bool _redirectDistinct = true;
};
}
namespace OCC {
Q_LOGGING_CATEGORY(lcCheckServerJob, "sync.checkserverjob", QtInfoMsg)
CheckServerJobResult::CheckServerJobResult() = default;
CheckServerJobResult::CheckServerJobResult(const QJsonObject &statusObject, const QUrl &serverUrl)
: _statusObject(statusObject)
, _serverUrl(serverUrl)
{
}
QJsonObject CheckServerJobResult::statusObject() const
{
return _statusObject;
}
QUrl CheckServerJobResult::serverUrl() const
{
return _serverUrl;
}
CheckServerJobFactory CheckServerJobFactory::createFromAccount(const AccountPtr &account, bool clearCookies, QObject *parent)
{
// in order to receive all ssl erorrs we need a fresh QNam
auto nam = account->credentials()->createAM();
nam->setCustomTrustedCaCertificates(account->approvedCerts());
nam->setParent(parent);
// do we start with the old cookies or new
if (!(clearCookies && Theme::instance()->connectionValidatorClearCookies())) {
const auto accountCookies = account->accessManager()->openCloudCookieJar()->allCookies();
nam->openCloudCookieJar()->setAllCookies(accountCookies);
}
return CheckServerJobFactory(nam);
}
CoreJob *CheckServerJobFactory::startJob(const QUrl &url, QObject *parent)
{
// the custom job class is used to store some state we need to maintain until the job has finished
auto req = makeRequest(Utility::concatUrlPath(url, QStringLiteral("status.php")));
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
req.setRawHeader(QByteArrayLiteral("OC-Connection-Validator"), QByteArrayLiteral("desktop"));
req.setMaximumRedirectsAllowed(_maxRedirectsAllowed);
auto job = new CheckServerCoreJob(nam()->get(req), parent);
QObject::connect(job->reply(), &QNetworkReply::redirected, job, [job] {
const auto code = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (code == 302 || code == 307) {
job->_redirectDistinct = false;
}
});
QObject::connect(job->reply(), &QNetworkReply::finished, job, [serverUrl = url, job]() mutable {
const QUrl targetUrl = job->reply()->url().adjusted(QUrl::RemoveFilename);
// TODO: still needed?
if (targetUrl.scheme() == QLatin1String("https")
&& job->reply()->sslConfiguration().sessionTicket().isEmpty()
&& job->reply()->error() == QNetworkReply::NoError) {
qCWarning(lcCheckServerJob) << u"No SSL session identifier / session ticket is used, this might impact sync performance negatively.";
}
if (!Utility::urlEqual(serverUrl, targetUrl)) {
if (job->_redirectDistinct) {
serverUrl = targetUrl;
} else {
qCWarning(lcCheckServerJob) << u"We got a temporary moved server aborting";
setJobError(job, QStringLiteral("Illegal redirect by server"));
return;
}
}
const int httpStatus = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (job->reply()->error() == QNetworkReply::TooManyRedirectsError) {
qCWarning(lcCheckServerJob) << u"error:" << job->reply()->errorString();
setJobError(job, job->reply()->errorString());
} else if (httpStatus != 200 || job->reply()->bytesAvailable() == 0) {
qCWarning(lcCheckServerJob) << u"error: status.php replied" << httpStatus;
setJobError(job, QStringLiteral("Invalid HTTP status code received for status.php: %1").arg(httpStatus));
} else {
const QByteArray body = job->reply()->peek(4 * 1024);
QJsonParseError error;
auto status = QJsonDocument::fromJson(body, &error);
// empty or invalid response
if (error.error != QJsonParseError::NoError || status.isNull()) {
qCWarning(lcCheckServerJob) << u"status.php from server is not valid JSON!" << body << job->reply()->request().url() << error.errorString();
}
qCInfo(lcCheckServerJob) << u"status.php returns: " << status << u" " << job->reply()->error() << u" Reply: " << job->reply();
if (status.object().contains(QStringLiteral("installed"))) {
CheckServerJobResult result(status.object(), serverUrl);
setJobResult(job, QVariant::fromValue(result));
} else {
qCWarning(lcCheckServerJob) << u"No proper answer on " << job->reply()->url();
setJobError(job, QStringLiteral("Did not receive expected reply from server"));
}
}
});
return job;
}
} // OCC
#include "checkserverjobfactory.moc"
@@ -0,0 +1,58 @@
/*
* Copyright (C) Fabian Müller <fmueller@owncloud.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 "abstractcorejob.h"
#include <QJsonObject>
namespace OCC {
class QSFERA_SYNC_EXPORT CheckServerJobResult
{
public:
CheckServerJobResult();
CheckServerJobResult(const QJsonObject &statusObject, const QUrl &serverUrl);
QJsonObject statusObject() const;
QUrl serverUrl() const;
private:
const QJsonObject _statusObject;
const QUrl _serverUrl;
};
class QSFERA_SYNC_EXPORT CheckServerJobFactory : public AbstractCoreJobFactory
{
public:
using AbstractCoreJobFactory::AbstractCoreJobFactory;
/**
* clearCookies: Whether to clear the cookies before we start the CheckServerJob job
* This option also depends on Theme::instance()->connectionValidatorClearCookies()
*/
static CheckServerJobFactory createFromAccount(const AccountPtr &account, bool clearCookies, QObject *parent);
CoreJob *startJob(const QUrl &url, QObject *parent) override;
private:
int _maxRedirectsAllowed = 5;
};
} // OCC
Q_DECLARE_METATYPE(OCC::CheckServerJobResult)
@@ -0,0 +1,89 @@
/*
* Copyright (C) Fabian Müller <fmueller@owncloud.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 "fetchuserinfojobfactory.h"
#include "common/utility.h"
#include "creds/httpcredentials.h"
#include <QApplication>
#include <QJsonParseError>
#include <QNetworkReply>
#include <QStringLiteral>
Q_LOGGING_CATEGORY(lcFetchUserInfoJob, "sync.networkjob.fetchuserinfojob", QtInfoMsg);
namespace OCC {
FetchUserInfoJobFactory FetchUserInfoJobFactory::fromBasicAuthCredentials(QNetworkAccessManager *nam, const QString &username, const QString &password)
{
QString authorizationHeader = QStringLiteral("Basic %1").arg(QString::fromUtf8(QStringLiteral("%1:%2").arg(username, password).toUtf8().toBase64()));
return { nam, authorizationHeader };
}
FetchUserInfoJobFactory FetchUserInfoJobFactory::fromOAuth2Credentials(QNetworkAccessManager *nam, const QString &bearerToken)
{
QString authorizationHeader = QStringLiteral("Bearer %1").arg(bearerToken);
return { nam, authorizationHeader };
}
FetchUserInfoJobFactory::FetchUserInfoJobFactory(QNetworkAccessManager *nam, const QString &authHeaderValue)
: AbstractCoreJobFactory(nam)
, _authorizationHeader(authHeaderValue)
{
}
CoreJob *FetchUserInfoJobFactory::startJob(const QUrl &url, QObject *parent)
{
QUrlQuery urlQuery({ { QStringLiteral("format"), QStringLiteral("json") } });
auto req = makeRequest(Utility::concatUrlPath(url, QStringLiteral("ocs/v2.php/cloud/user"), urlQuery));
// We are not connected yet so we need to handle the authentication manually
req.setRawHeader("Authorization", _authorizationHeader.toUtf8());
req.setRawHeader(QByteArrayLiteral("OCS-APIREQUEST"), QByteArrayLiteral("true"));
// We just added the Authorization header, don't let HttpCredentialsAccessManager tamper with it
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
req.setAttribute(QNetworkRequest::AuthenticationReuseAttribute, QNetworkRequest::Manual);
auto *job = new CoreJob(nam()->get(req), parent);
QObject::connect(job->reply(), &QNetworkReply::finished, job, [job] {
const auto data = job->reply()->readAll();
const auto statusCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (job->reply()->error() != QNetworkReply::NoError || statusCode != 200) {
setJobError(job, QApplication::translate("FetchUserInfoJobFactory", "Failed to retrieve user info"));
} else {
qCDebug(lcFetchUserInfoJob) << data;
QJsonParseError error = {};
const auto json = QJsonDocument::fromJson(data, &error);
if (error.error == QJsonParseError::NoError) {
const auto jsonData = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toObject();
FetchUserInfoResult result(jsonData.value(QStringLiteral("id")).toString(), jsonData.value(QStringLiteral("display-name")).toString());
setJobResult(job, QVariant::fromValue(result));
} else {
setJobError(job, error.errorString());
}
}
});
return job;
}
} // OCC
@@ -0,0 +1,62 @@
/*
* Copyright (C) Fabian Müller <fmueller@owncloud.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 "abstractcorejob.h"
namespace OCC {
class QSFERA_SYNC_EXPORT FetchUserInfoResult
{
public:
FetchUserInfoResult() = default;
FetchUserInfoResult(const QString &userName, const QString &displayName)
{
_userName = userName;
_displayName = displayName;
}
QString userName() const
{
return _userName;
}
QString displayName() const
{
return _displayName;
};
private:
QString _userName;
QString _displayName;
};
class QSFERA_SYNC_EXPORT FetchUserInfoJobFactory : public AbstractCoreJobFactory
{
public:
static FetchUserInfoJobFactory fromBasicAuthCredentials(QNetworkAccessManager *nam, const QString &username, const QString &token);
static FetchUserInfoJobFactory fromOAuth2Credentials(QNetworkAccessManager *nam, const QString &bearerToken);
CoreJob *startJob(const QUrl &url, QObject *parent) override;
private:
FetchUserInfoJobFactory(QNetworkAccessManager *nam, const QString &authHeaderValue);
QString _authorizationHeader;
};
} // OCC
Q_DECLARE_METATYPE(OCC::FetchUserInfoResult)
@@ -0,0 +1,276 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#include "libsync/networkjobs/getfilejob.h"
#include "libsync/owncloudpropagator_p.h"
using namespace OCC;
Q_LOGGING_CATEGORY(lcGetJob, "sync.networkjob.get", QtInfoMsg)
// DOES NOT take ownership of the device.
GETFileJob::GETFileJob(AccountPtr account, const QUrl &url, const QString &path, QIODevice *device, const QMap<QByteArray, QByteArray> &headers,
const QString &expectedEtagForResume, qint64 resumeStart, QObject *parent)
: AbstractNetworkJob(account, url, path, parent)
, _device(device)
, _headers(headers)
, _expectedEtagForResume(expectedEtagForResume)
, _expectedContentLength(-1)
, _contentLength(-1)
, _resumeStart(resumeStart)
{
connect(this, &GETFileJob::networkError, this, [this] {
if (timedOut()) {
qCWarning(lcGetJob) << this << u"timeout";
_errorString = tr("Connection Timeout");
_errorStatus = SyncFileItem::FatalError;
}
});
// Long downloads must not block non-propagation jobs.
setPriority(QNetworkRequest::LowPriority);
}
void GETFileJob::start()
{
if (_resumeStart > 0) {
_headers["Range"] = "bytes=" + QByteArray::number(_resumeStart) + '-';
_headers["Accept-Ranges"] = "bytes";
qCDebug(lcGetJob) << u"Retry with range " << _headers["Range"];
}
QNetworkRequest req;
for (auto it = _headers.cbegin(); it != _headers.cend(); ++it) {
req.setRawHeader(it.key(), it.value());
}
if (_bandwidthManager) {
// probably a qt bug, with http2 we might handle the input too slow causing the whole file to be buffered by qt in ram
req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
}
sendRequest("GET", req);
qCDebug(lcGetJob) << _bandwidthManager << _bandwidthChoked << _bandwidthLimited;
if (_bandwidthManager) {
_bandwidthManager->registerDownloadJob(this);
}
AbstractNetworkJob::start();
}
void GETFileJob::finished()
{
if (_bandwidthManager) {
_bandwidthManager->unregisterDownloadJob(this);
}
if (reply()->bytesAvailable() && _httpOk) {
// we were throttled, write out the remaining data
slotReadyRead();
Q_ASSERT(!reply()->bytesAvailable());
}
// ensure the device is closed in case the underlying file is modified in a signal connected to finished()
_device->close();
}
void GETFileJob::newReplyHook(QNetworkReply *reply)
{
if (_bandwidthManager) {
reply->setReadBufferSize(16 * 1024); // keep low so we can easier limit the bandwidth
}
connect(reply, &QNetworkReply::metaDataChanged, this, &GETFileJob::slotMetaDataChanged);
connect(reply, &QNetworkReply::finished, this, &GETFileJob::slotReadyRead);
connect(reply, &QNetworkReply::downloadProgress, this, &GETFileJob::downloadProgress);
}
void GETFileJob::slotMetaDataChanged()
{
// For some reason setting the read buffer in GETFileJob::start doesn't seem to go
// through the HTTP layer thread(?)
if (_bandwidthManager) {
reply()->setReadBufferSize(16 * 1024);
}
int httpStatus = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (httpStatus == 301 || httpStatus == 302 || httpStatus == 303 || httpStatus == 307 || httpStatus == 308 || httpStatus == 401) {
// Redirects and auth failures (OAuth token renew) are handled by AbstractNetworkJob and
// will end up restarting the job. We do not want to process further data from the initial
// request. newReplyHook() will reestablish signal connections for the follow-up request.
disconnect(reply(), &QNetworkReply::finished, this, &GETFileJob::slotReadyRead);
disconnect(reply(), &QNetworkReply::readyRead, this, &GETFileJob::slotReadyRead);
return;
}
// If the status code isn't 2xx, don't write the reply body to the file.
// For any error: handle it when the job is finished, not here.
if (httpStatus / 100 != 2) {
// Disable the buffer limit, as we don't limit the bandwidth for error messages.
// (We are only going to do a readAll() at the end.)
if (_bandwidthManager) {
reply()->setReadBufferSize(0);
}
return;
}
if (reply()->error() != QNetworkReply::NoError) {
return;
}
_etag = getEtagFromReply(reply());
if (_etag.isEmpty()) {
qCWarning(lcGetJob) << u"No E-Tag reply by server, considering it invalid";
_errorString = tr("No E-Tag received from server, check Proxy/Gateway");
_errorStatus = SyncFileItem::NormalError;
abort();
return;
} else if (!_expectedEtagForResume.isEmpty() && _expectedEtagForResume != _etag) {
qCWarning(lcGetJob) << u"We received a different E-Tag for resuming!" << _expectedEtagForResume << u"vs" << _etag;
_errorString = tr("We received a different E-Tag for resuming. Retrying next time.");
_errorStatus = SyncFileItem::NormalError;
abort();
return;
}
bool ok;
_contentLength = reply()->header(QNetworkRequest::ContentLengthHeader).toLongLong(&ok);
if (ok && _expectedContentLength != -1 && _contentLength != _expectedContentLength) {
qCWarning(lcGetJob) << u"We received a different content length than expected!" << _expectedContentLength << u"vs" << _contentLength;
_errorString = tr("We received an unexpected download Content-Length.");
_errorStatus = SyncFileItem::NormalError;
abort();
return;
}
qint64 start = 0;
const QString ranges = QString::fromUtf8(reply()->rawHeader("Content-Range"));
if (!ranges.isEmpty()) {
static QRegularExpression rx(QStringLiteral("bytes (\\d+)-"));
const auto match = rx.match(ranges);
if (match.hasMatch()) {
start = match.captured(1).toLongLong();
}
}
if (start != _resumeStart) {
qCWarning(lcGetJob) << u"Wrong content-range: " << ranges << u" while expecting start was" << _resumeStart;
if (ranges.isEmpty()) {
// device doesn't support range, just try again from scratch
_device->close();
if (!_device->open(QIODevice::WriteOnly)) {
_errorString = _device->errorString();
_errorStatus = SyncFileItem::NormalError;
abort();
return;
}
_resumeStart = 0;
} else {
_errorString = tr("Server returned wrong content-range");
_errorStatus = SyncFileItem::NormalError;
abort();
return;
}
}
auto lastModified = reply()->header(QNetworkRequest::LastModifiedHeader);
if (!lastModified.isNull()) {
_lastModified = Utility::qDateTimeToTime_t(lastModified.toDateTime());
}
_httpOk = true;
connect(reply(), &QIODevice::readyRead, this, &GETFileJob::slotReadyRead);
}
void GETFileJob::setBandwidthManager(BandwidthManager *bwm)
{
_bandwidthManager = bwm;
}
void GETFileJob::setChoked(bool c)
{
if (c != _bandwidthChoked) {
_bandwidthChoked = c;
QMetaObject::invokeMethod(this, &GETFileJob::slotReadyRead, Qt::QueuedConnection);
}
}
void GETFileJob::setBandwidthLimited(bool b)
{
if (_bandwidthLimited != b) {
_bandwidthLimited = b;
QMetaObject::invokeMethod(this, &GETFileJob::slotReadyRead, Qt::QueuedConnection);
}
}
void GETFileJob::giveBandwidthQuota(qint64 q)
{
_bandwidthQuota = q;
qCDebug(lcGetJob) << u"Got" << q << u"bytes";
QMetaObject::invokeMethod(this, &GETFileJob::slotReadyRead, Qt::QueuedConnection);
}
qint64 GETFileJob::currentDownloadPosition()
{
if (_device && _device->pos() > 0 && _device->pos() > qint64(_resumeStart)) {
return _device->pos();
}
return _resumeStart;
}
void GETFileJob::slotReadyRead()
{
Q_ASSERT(reply());
if (!_httpOk) {
return;
}
const qint64 bufferSize = std::min<qint64>(1024 * 8, reply()->bytesAvailable());
QByteArray buffer(bufferSize, Qt::Uninitialized);
while (reply()->bytesAvailable() > 0) {
if (_bandwidthChoked) {
qCWarning(lcGetJob) << u"Download choked";
break;
}
qint64 toRead = bufferSize;
if (_bandwidthLimited) {
toRead = std::min<qint64>(bufferSize, _bandwidthQuota);
if (toRead == 0) {
qCWarning(lcGetJob) << u"Out of bandwidth quota";
break;
}
_bandwidthQuota -= toRead;
}
const qint64 read = reply()->read(buffer.data(), toRead);
if (read < 0) {
_errorString = networkReplyErrorString(*reply());
_errorStatus = SyncFileItem::NormalError;
qCWarning(lcGetJob) << u"Error while reading from device: " << _errorString;
abort();
return;
}
const qint64 written = _device->write(buffer.constData(), read);
if (written != read) {
_errorString = _device->errorString();
_errorStatus = SyncFileItem::NormalError;
qCWarning(lcGetJob) << u"Error while writing to file" << written << read << _errorString;
abort();
return;
}
}
}
GETFileJob::~GETFileJob()
{
if (_bandwidthManager) {
_bandwidthManager->unregisterDownloadJob(this);
}
}
QString GETFileJob::errorString() const
{
if (!_errorString.isEmpty()) {
return _errorString;
}
return AbstractNetworkJob::errorString();
}
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#pragma once
#include "libsync/abstractnetworkjob.h"
#include "libsync/accountfwd.h"
#include "libsync/bandwidthmanager.h"
#include "libsync/qsferasynclib.h"
#include "libsync/syncfileitem.h"
namespace OCC {
/**
* @brief Downloads the remote file via GET
* @ingroup libsync
*/
class QSFERA_SYNC_EXPORT GETFileJob : public AbstractNetworkJob
{
Q_OBJECT
QIODevice *_device;
QMap<QByteArray, QByteArray> _headers;
QString _expectedEtagForResume;
qint64 _expectedContentLength;
qint64 _contentLength;
qint64 _resumeStart;
public:
// DOES NOT take ownership of the device.
// For directDownloadUrl:
explicit GETFileJob(AccountPtr account, const QUrl &url, const QString &path, QIODevice *device, const QMap<QByteArray, QByteArray> &headers,
const QString &expectedEtagForResume, qint64 resumeStart, QObject *parent = nullptr);
virtual ~GETFileJob();
qint64 currentDownloadPosition();
void start() override;
void finished() override;
void newReplyHook(QNetworkReply *reply) override;
qint64 resumeStart() { return _resumeStart; }
qint64 contentLength() const { return _contentLength; }
qint64 expectedContentLength() const { return _expectedContentLength; }
void setExpectedContentLength(qint64 size) { _expectedContentLength = size; }
void setChoked(bool c);
void setBandwidthLimited(bool b);
void giveBandwidthQuota(qint64 q);
void setBandwidthManager(BandwidthManager *bwm);
QString &etag() { return _etag; }
time_t lastModified() { return _lastModified; }
void setErrorString(const QString &s) { _errorString = s; }
QString errorString() const;
SyncFileItem::Status errorStatus() { return _errorStatus; }
void setErrorStatus(const SyncFileItem::Status &s) { _errorStatus = s; }
private Q_SLOTS:
void slotReadyRead();
void slotMetaDataChanged();
Q_SIGNALS:
void downloadProgress(qint64, qint64);
protected:
bool restartDevice();
QString _etag;
time_t _lastModified = 0;
QString _errorString;
SyncFileItem::Status _errorStatus = SyncFileItem::NoStatus;
bool _bandwidthLimited = false; // if _bandwidthQuota will be used
bool _bandwidthChoked = false; // if download is paused (won't read on readyRead())
qint64 _bandwidthQuota = 0;
bool _httpOk = false;
QPointer<BandwidthManager> _bandwidthManager = nullptr;
};
}
+116
View File
@@ -0,0 +1,116 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.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 "jsonjob.h"
#include "account.h"
#include "common/utility.h"
#include <QJsonDocument>
#include <QLoggingCategory>
#include <QNetworkReply>
Q_LOGGING_CATEGORY(lcJsonApiJob, "sync.networkjob.jsonapi", QtInfoMsg)
using namespace OCC;
JsonJob::~JsonJob()
{
}
void JsonJob::finished()
{
if (reply()->error() != QNetworkReply::NoError) {
qCWarning(lcJsonApiJob) << u"Network error: " << this << errorString();
} else {
parse(reply()->readAll());
}
SimpleNetworkJob::finished();
}
void JsonJob::parse(const QByteArray &data)
{
const auto doc = QJsonDocument::fromJson(data, &_parseError);
// empty or invalid response
if (_parseError.error != QJsonParseError::NoError || doc.isNull()) {
qCWarning(lcJsonApiJob) << u"invalid JSON!" << data << _parseError.errorString();
} else {
_data = doc.object();
}
}
const QJsonParseError &JsonJob::parseError() const
{
return _parseError;
}
const QJsonObject &JsonJob::data() const
{
return _data;
}
JsonApiJob::JsonApiJob(AccountPtr account, const QString &path, const QByteArray &verb, const UrlQuery &arguments, const QNetworkRequest &req, QObject *parent)
: JsonJob(account, account->url(), path, verb, arguments, req, parent)
{
_request.setRawHeader(QByteArrayLiteral("OCS-APIREQUEST"), QByteArrayLiteral("true"));
auto q = query();
// HACK: q will be empty for POST, PUT, PATCH request as we utilise the body (see SimpleNetworkJob)
// however the oc api requires the format header to be part of the url
q.addQueryItem(QStringLiteral("format"), QStringLiteral("json"));
setQuery(q);
}
JsonApiJob::JsonApiJob(AccountPtr account, const QString &path, const UrlQuery &arguments, const QNetworkRequest &req, QObject *parent)
: JsonApiJob(account, path, QByteArrayLiteral("GET"), arguments, req, parent)
{
}
int JsonApiJob::ocsStatus() const
{
return _ocsStatus;
}
void JsonApiJob::parse(const QByteArray &rawData)
{
static const QRegularExpression rex(QStringLiteral("<statuscode>(\\d+)</statuscode>"));
const auto match = rex.match(QString::fromUtf8(rawData));
if (match.hasMatch()) {
// this is a error message coming back from ocs.
_ocsStatus = match.captured(1).toInt();
} else {
JsonJob::parse(rawData);
// example: "{"ocs":{"meta":{"status":"ok","statuscode":100,"message":null},"data":{"version":{"major":8,"minor":"... (504)
if (parseError().error == QJsonParseError::NoError) {
if (data().contains(QLatin1String("ocs"))) {
const auto meta = data().value(QLatin1String("ocs")).toObject().value(QLatin1String("meta")).toObject();
_ocsStatus = meta.value(QLatin1String("statuscode")).toInt();
_ocsMessage = meta.value(QLatin1String("message")).toString();
} else {
_ocsMessage = parseError().errorString();
}
}
}
}
const QString &JsonApiJob::ocsMessage() const
{
return _ocsMessage;
}
bool JsonApiJob::ocsSuccess() const
{
// v1 api: 100
// v2 api: 200 as in http
return ocsStatus() == 100 || ocsStatus() == 200;
}
+85
View File
@@ -0,0 +1,85 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.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 "libsync/networkjobs/simplenetworkjob.h"
#include <QJsonObject>
#include <QJsonParseError>
namespace OCC {
class QSFERA_SYNC_EXPORT JsonJob : public SimpleNetworkJob
{
Q_OBJECT
public:
using SimpleNetworkJob::SimpleNetworkJob;
virtual ~JsonJob();
const QJsonObject &data() const;
const QJsonParseError &parseError() const;
protected:
void finished() override;
virtual void parse(const QByteArray &data);
private:
QJsonParseError _parseError;
QJsonObject _data;
};
/**
* @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 QSFERA_SYNC_EXPORT JsonApiJob : public JsonJob
{
Q_OBJECT
public:
explicit JsonApiJob(AccountPtr account, const QString &path, const QByteArray &verb, const UrlQuery &arguments, const QNetworkRequest &req, QObject *parent);
explicit JsonApiJob(AccountPtr account, const QString &path, const UrlQuery &arguments, const QNetworkRequest &req, QObject *parent);
// the OCS status code: 100 (!) for success
int ocsStatus() const;
const QString &ocsMessage() const;
// whether the job was successful
bool ocsSuccess() const;
private:
int _ocsStatus = 0;
QString _ocsMessage;
protected:
using JsonJob::JsonJob;
virtual void parse(const QByteArray &data) override;
};
}
@@ -0,0 +1,119 @@
#include "libsync/networkjobs/resources.h"
#include "libsync/account.h"
#include <QFile>
#include <QIcon>
#include <QMimeDatabase>
namespace OCC {
Q_LOGGING_CATEGORY(lcResources, "sync.networkjob.resource")
namespace {
QString hashMd5(const QString &data)
{
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
hash.addData(data.toLatin1());
return QString::fromLatin1(hash.result().toHex());
}
QString cacheKey(QNetworkReply *reply)
{
const QString urlHash = hashMd5(reply->url().toString());
const QString eTagHash = hashMd5(reply->header(QNetworkRequest::ETagHeader).toString());
const auto db = QMimeDatabase();
QString extension = db.mimeTypeForName(reply->header(QNetworkRequest::ContentTypeHeader).toString()).preferredSuffix();
if (extension.isEmpty()) {
extension = db.mimeTypeForData(reply).preferredSuffix();
}
if (extension.isEmpty()) {
extension = QStringLiteral("unknown");
}
return QStringLiteral("%1-%2.%3").arg(urlHash, eTagHash, extension);
}
}
void ResourceJob::finished()
{
if (reply()->error() != QNetworkReply::NoError) {
qCWarning(lcResources) << u"Network error: " << this << errorString();
} else {
_cacheKey = cacheKey(reply());
}
// storing the file on disk enables Qt to apply some optimizations, e.g., in QIcon
// also, specifically for icons, loading from disk allows easy management of scalable and non-scalable entities
if (httpStatusCode() == 200 && reply()->size() > 0) {
const QString path = _cache->path(_cacheKey);
qCDebug(lcResources) << u"cache file path:" << path;
// furthermore, we can skip writing the file if the cache key has not changed (i.e., a file exists) and the file has come from the network cache
if (QFileInfo::exists(path) && reply()->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()) {
qCDebug(lcResources) << u"file has come from network cache, skipping writing";
} else {
QFile cacheFile(path);
// note: we want to truncate the file if it exists
if (!cacheFile.open(QIODevice::WriteOnly)) {
qCCritical(lcResources) << u"failed to open cache file for writing:" << cacheFile.fileName();
} else {
if (!cacheFile.write(reply()->readAll())) {
qCCritical(lcResources) << u"failed to write to cache file:" << cacheFile.fileName();
}
}
}
}
SimpleNetworkJob::finished();
}
QIcon ResourceJob::asIcon() const
{
// storing the file on disk enables Qt to apply some optimizations (e.g., caching of rendered pixmaps)
Q_ASSERT(!_cacheKey.isEmpty());
return QIcon(_cache->path(_cacheKey));
}
ResourceJob::ResourceJob(const ResourcesCache *cache, const QUrl &rootUrl, const QString &path, QObject *parent)
: SimpleNetworkJob(cache->account()->sharedFromThis(), rootUrl, path, "GET", {}, parent)
, _cache(cache)
{
setStoreInCache(true);
}
ResourcesCache::ResourcesCache(const QString &cacheDirectory, Account *account)
: QObject(account)
, _account(account)
, _cacheDirectory(QStringLiteral("%1/tmp.XXXXXX").arg(cacheDirectory))
{
Q_ASSERT(_cacheDirectory.isValid());
}
ResourceJob *ResourcesCache::makeGetJob(const QUrl &rootUrl, const QString &path, QObject *parent) const
{
return new ResourceJob(this, rootUrl, path, parent);
}
ResourceJob *ResourcesCache::makeGetJob(const QString &path, QObject *parent) const
{
return makeGetJob(_account->url(), path, parent);
}
Account *ResourcesCache::account() const
{
return _account;
}
QString ResourcesCache::path(const QString &cacheKey) const
{
Q_ASSERT(!cacheKey.isEmpty());
Q_ASSERT(_cacheDirectory.isValid());
return _cacheDirectory.filePath(cacheKey);
}
}
@@ -0,0 +1,60 @@
#pragma once
#include "libsync/networkjobs/simplenetworkjob.h"
#include <QIcon>
#include <QLoggingCategory>
#include <QTemporaryDir>
namespace OCC {
class ResourcesCache;
/**
* This job automatically downloads all available data from the server and stores it in a temporary cache directory on the disk.
* For convenience, a couple of conversion functions are available to convert the binary data to common Qt classes such as QIcon.
*/
class QSFERA_SYNC_EXPORT ResourceJob : public SimpleNetworkJob
{
public:
void finished() override;
QIcon asIcon() const;
protected:
explicit ResourceJob(const ResourcesCache *cache, const QUrl &rootUrl, const QString &path, QObject *parent);
private:
const ResourcesCache *_cache;
QString _cacheKey;
friend class ResourcesCache;
};
class QSFERA_SYNC_EXPORT ResourcesCache : public QObject
{
Q_OBJECT
public:
/**
*
* @param cacheDirectory path to temporary cache parent directory (note: this directory must exist)
* @param account
*/
explicit ResourcesCache(const QString &cacheDirectory, Account *account);
ResourceJob *makeGetJob(const QUrl &rootUrl, const QString &path, QObject *parent) const;
ResourceJob *makeGetJob(const QString &path, QObject *parent) const;
Account *account() const;
QString path(const QString &cacheKey) const;
private:
Account *_account;
QTemporaryDir _cacheDirectory;
};
} // namespace OCC
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#include "simplenetworkjob.h"
#include <QBuffer>
#include <QJsonDocument>
using namespace OCC;
SimpleNetworkJob::SimpleNetworkJob(
AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const QNetworkRequest &req, QObject *parent)
: AbstractNetworkJob(account, rootUrl, path, parent)
, _request(req)
, _verb(verb)
{
}
SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, QObject *parent)
: SimpleNetworkJob(account, rootUrl, path, verb, {}, parent)
{
}
SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const UrlQuery &arguments,
const QNetworkRequest &req, QObject *parent)
: SimpleNetworkJob(account, rootUrl, path, verb, req, parent)
{
Q_ASSERT((QList<QByteArray>{"GET", "PUT", "POST", "DELETE", "HEAD", "PATCH"}.contains(verb)));
if (!arguments.isEmpty()) {
QUrlQuery args;
// ensure everything is percent encoded
// this is especially important for parameters that contain spaces or +
for (const auto &item : arguments) {
args.addQueryItem(QString::fromUtf8(QUrl::toPercentEncoding(item.first)), QString::fromUtf8(QUrl::toPercentEncoding(item.second)));
}
if (verb == QByteArrayLiteral("POST") || verb == QByteArrayLiteral("PUT") || verb == QByteArrayLiteral("PATCH")) {
_request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded; charset=UTF-8"));
_body = args.query(QUrl::FullyEncoded).toUtf8();
_device = std::make_unique<QBuffer>(&_body);
} else {
setQuery(args);
}
}
}
SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const QJsonObject &arguments,
const QNetworkRequest &req, QObject *parent)
: SimpleNetworkJob(account, rootUrl, path, verb, QJsonDocument(arguments).toJson(), req, parent)
{
_request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
}
SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb,
std::unique_ptr<QIODevice> &&requestBody, const QNetworkRequest &req, QObject *parent)
: SimpleNetworkJob(account, rootUrl, path, verb, req, parent)
{
_device = std::move(requestBody);
}
// delay setting _device until we adopted the body
SimpleNetworkJob::SimpleNetworkJob(
AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, QByteArray &&requestBody, const QNetworkRequest &req, QObject *parent)
: SimpleNetworkJob(account, rootUrl, path, verb, std::unique_ptr<QIODevice>{}, req, parent)
{
_body = std::move(requestBody);
_device = std::make_unique<QBuffer>(&_body);
}
SimpleNetworkJob::~SimpleNetworkJob() { }
void SimpleNetworkJob::start()
{
Q_ASSERT(!_verb.isEmpty());
// AbstractNetworkJob will take ownership of the buffer
sendRequest(_verb, _request, std::move(_device));
AbstractNetworkJob::start();
}
void SimpleNetworkJob::addNewReplyHook(std::function<void(QNetworkReply *)> &&hook)
{
_replyHooks.push_back(hook);
}
void SimpleNetworkJob::finished()
{
if (_device) {
_device->close();
}
if (body()) {
body()->close();
}
}
void SimpleNetworkJob::newReplyHook(QNetworkReply *reply)
{
for (const auto &hook : _replyHooks) {
hook(reply);
}
}
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#pragma once
#include "libsync/abstractcorejob.h"
namespace OCC {
/**
* @brief A basic job around a network request without extra funtionality
* @ingroup libsync
*
* Primarily adds timeout and redirection handling.
*/
class QSFERA_SYNC_EXPORT SimpleNetworkJob : public AbstractNetworkJob
{
Q_OBJECT
public:
using UrlQuery = QList<QPair<QString, QString>>;
// fully qualified urls can be passed in the QNetworkRequest
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, QObject *parent);
explicit SimpleNetworkJob(
AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const QNetworkRequest &req = {}, QObject *parent = nullptr);
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb,
std::unique_ptr<QIODevice> &&requestBody = {}, const QNetworkRequest &req = {}, QObject *parent = nullptr);
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const UrlQuery &arguments,
const QNetworkRequest &req = {}, QObject *parent = nullptr);
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const QJsonObject &arguments,
const QNetworkRequest &req = {}, QObject *parent = nullptr);
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, QByteArray &&requestBody,
const QNetworkRequest &req = {}, QObject *parent = nullptr);
virtual ~SimpleNetworkJob();
void start() override;
void addNewReplyHook(std::function<void(QNetworkReply *)> &&hook);
protected:
void finished() override;
void newReplyHook(QNetworkReply *) override;
QNetworkRequest _request;
private:
QByteArray _verb;
QByteArray _body;
std::unique_ptr<QIODevice> _device;
std::vector<std::function<void(QNetworkReply *)>> _replyHooks;
};
}