From b94ea70731fb8ecda0659435d93969fe8ddbafb1 Mon Sep 17 00:00:00 2001 From: alex-z Date: Tue, 21 Nov 2023 20:35:38 +0100 Subject: [PATCH 1/9] Client status reporting feature. First iteration. Signed-off-by: alex-z --- src/common/clientstatusreportingrecord.cpp | 24 ++ src/common/clientstatusreportingrecord.h | 34 +++ src/common/common.cmake | 1 + src/gui/CMakeLists.txt | 4 +- src/libsync/CMakeLists.txt | 8 +- src/libsync/account.cpp | 32 ++- src/libsync/account.h | 7 + src/libsync/clientstatusreporting.cpp | 283 ++++++++++++++++++++ src/libsync/clientstatusreporting.h | 71 +++++ src/libsync/ocsclientstatusreportingjob.cpp | 46 ++++ src/libsync/ocsclientstatusreportingjob.h | 42 +++ src/{gui => libsync}/ocsjob.cpp | 0 src/{gui => libsync}/ocsjob.h | 8 +- 13 files changed, 544 insertions(+), 16 deletions(-) create mode 100644 src/common/clientstatusreportingrecord.cpp create mode 100644 src/common/clientstatusreportingrecord.h create mode 100644 src/libsync/clientstatusreporting.cpp create mode 100644 src/libsync/clientstatusreporting.h create mode 100644 src/libsync/ocsclientstatusreportingjob.cpp create mode 100644 src/libsync/ocsclientstatusreportingjob.h rename src/{gui => libsync}/ocsjob.cpp (100%) rename src/{gui => libsync}/ocsjob.h (99%) diff --git a/src/common/clientstatusreportingrecord.cpp b/src/common/clientstatusreportingrecord.cpp new file mode 100644 index 0000000000000..0211b271ccbc8 --- /dev/null +++ b/src/common/clientstatusreportingrecord.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "clientstatusreportingrecord.h" + +namespace OCC +{ + +bool ClientStatusReportingRecord::isValid() const +{ + return !_name.isEmpty() && _nameHash > 0 && _lastOccurence > 0; +} +} diff --git a/src/common/clientstatusreportingrecord.h b/src/common/clientstatusreportingrecord.h new file mode 100644 index 0000000000000..a1aff6df09e23 --- /dev/null +++ b/src/common/clientstatusreportingrecord.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "ocsynclib.h" + +#include +#include + +namespace OCC +{ +/** + * @brief The ClientStatusReportingRecord class + * @ingroup libsync + */ +struct OCSYNC_EXPORT ClientStatusReportingRecord { + QByteArray _name; + quint64 _nameHash = 0; + quint64 _numOccurences = 1; + quint64 _lastOccurence = 0; + + [[nodiscard]] bool isValid() const; +}; +} diff --git a/src/common/common.cmake b/src/common/common.cmake index 671973579f0bb..f25151ebbd95e 100644 --- a/src/common/common.cmake +++ b/src/common/common.cmake @@ -4,6 +4,7 @@ set(common_SOURCES ${CMAKE_CURRENT_LIST_DIR}/checksums.cpp ${CMAKE_CURRENT_LIST_DIR}/checksumcalculator.cpp + ${CMAKE_CURRENT_LIST_DIR}/clientstatusreportingrecord.cpp ${CMAKE_CURRENT_LIST_DIR}/filesystembase.cpp ${CMAKE_CURRENT_LIST_DIR}/ownsql.cpp ${CMAKE_CURRENT_LIST_DIR}/preparedsqlquerymanager.cpp diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 1968683e23415..3befe15452465 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -121,10 +121,10 @@ set(client_SRCS navigationpanehelper.cpp networksettings.h networksettings.cpp + "${CMAKE_SOURCE_DIR}/src/libsync/ocsjob.h" + "${CMAKE_SOURCE_DIR}/src/libsync/ocsjob.cpp" ocsnavigationappsjob.h ocsnavigationappsjob.cpp - ocsjob.h - ocsjob.cpp ocssharejob.h ocssharejob.cpp ocsshareejob.h diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index fab99be586e3c..2944246e13d94 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -24,6 +24,7 @@ set(libsync_SRCS capabilities.cpp clientproxy.h clientproxy.cpp + clientstatusreporting.cpp cookiejar.h cookiejar.cpp discovery.h @@ -53,6 +54,10 @@ set(libsync_SRCS owncloudpropagator.cpp nextcloudtheme.h nextcloudtheme.cpp + ocsjob.h + ocsjob.cpp + ocsclientstatusreportingjob.h + ocsclientstatusreportingjob.cpp abstractpropagateremotedeleteencrypted.h abstractpropagateremotedeleteencrypted.cpp deletejob.h @@ -171,7 +176,7 @@ IF (NOT APPLE) ) ENDIF(NOT APPLE) -find_package(Qt5 REQUIRED COMPONENTS WebSockets Xml) +find_package(Qt5 REQUIRED COMPONENTS WebSockets Xml Sql) add_library(nextcloudsync SHARED ${libsync_SRCS}) add_library(Nextcloud::sync ALIAS nextcloudsync) @@ -186,6 +191,7 @@ target_link_libraries(nextcloudsync Qt5::Network Qt5::WebSockets Qt5::Xml + Qt5::Sql ) if (NOT TOKEN_AUTH_ONLY) diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 9e9101b01e6b3..24a1562ac25bc 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -17,6 +17,7 @@ #include "accountfwd.h" #include "capabilities.h" #include "clientsideencryptionjobs.h" +#include "clientstatusreporting.h" #include "configfile.h" #include "cookiejar.h" #include "creds/abstractcredentials.h" @@ -64,7 +65,8 @@ constexpr int checksumRecalculateRequestServerVersionMinSupportedMajor = 24; constexpr auto isSkipE2eeMetadataChecksumValidationAllowedInClientVersion = MIRALL_VERSION_MAJOR == 3 && MIRALL_VERSION_MINOR == 8; } -namespace OCC { +namespace OCC +{ Q_LOGGING_CATEGORY(lcAccount, "nextcloud.sync.account", QtInfoMsg) const char app_password[] = "_app-password"; @@ -87,7 +89,7 @@ AccountPtr Account::create() return acc; } -ClientSideEncryption* Account::e2e() +ClientSideEncryption *Account::e2e() { // Qt expects everything in the connect to be a pointer, so return a pointer. return &_e2e; @@ -267,14 +269,10 @@ void Account::setCredentials(AbstractCredentials *cred) if (proxy.type() != QNetworkProxy::DefaultProxy) { _am->setProxy(proxy); } - connect(_am.data(), &QNetworkAccessManager::sslErrors, - this, &Account::slotHandleSslErrors); - connect(_am.data(), &QNetworkAccessManager::proxyAuthenticationRequired, - this, &Account::proxyAuthenticationRequired); - connect(_credentials.data(), &AbstractCredentials::fetched, - this, &Account::slotCredentialsFetched); - connect(_credentials.data(), &AbstractCredentials::asked, - this, &Account::slotCredentialsAsked); + connect(_am.data(), &QNetworkAccessManager::sslErrors, this, &Account::slotHandleSslErrors); + connect(_am.data(), &QNetworkAccessManager::proxyAuthenticationRequired, this, &Account::proxyAuthenticationRequired); + connect(_credentials.data(), &AbstractCredentials::fetched, this, &Account::slotCredentialsFetched); + connect(_credentials.data(), &AbstractCredentials::asked, this, &Account::slotCredentialsAsked); trySetupPushNotifications(); } @@ -284,6 +282,18 @@ void Account::setPushNotificationsReconnectInterval(int interval) _pushNotificationsReconnectTimer.setInterval(interval); } +void Account::trySetupClientStatusReporting() +{ + _clientStatusReporting.reset(new ClientStatusReporting(this)); +} + +void Account::reportClientStatus(const int status) +{ + if (_clientStatusReporting) { + _clientStatusReporting->reportClientStatus(static_cast(status)); + } +} + void Account::trySetupPushNotifications() { // Stop the timer to prevent parallel setup attempts @@ -669,6 +679,8 @@ void Account::setCapabilities(const QVariantMap &caps) setupUserStatusConnector(); trySetupPushNotifications(); + + trySetupClientStatusReporting(); } void Account::setupUserStatusConnector() diff --git a/src/libsync/account.h b/src/libsync/account.h index 523fc7137cd9d..98aa1cb021f95 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -55,6 +55,7 @@ class AbstractCredentials; class Account; using AccountPtr = QSharedPointer; class AccessManager; +class ClientStatusReporting; class SimpleNetworkJob; class PushNotifications; class UserStatusConnector; @@ -305,6 +306,10 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject [[nodiscard]] PushNotifications *pushNotifications() const; void setPushNotificationsReconnectInterval(int interval); + void trySetupClientStatusReporting(); + + void reportClientStatus(const int status); + [[nodiscard]] std::shared_ptr userStatusConnector() const; void setLockFileState(const QString &serverRelativePath, @@ -439,6 +444,8 @@ private slots: PushNotifications *_pushNotifications = nullptr; + QScopedPointer _clientStatusReporting; + std::shared_ptr _userStatusConnector; QHash> _lockStatusChangeInprogress; diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp new file mode 100644 index 0000000000000..d2985e5954eec --- /dev/null +++ b/src/libsync/clientstatusreporting.cpp @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "clientstatusreporting.h" +#include "creds/abstractcredentials.h" +#include "account.h" +#include "common/clientstatusreportingrecord.h" +#include "common/syncjournaldb.h" +#include +#include + +namespace +{ +constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; +constexpr auto repordSendIntervalMs = 24 * 60 * 60 * 1000; +constexpr int clientStatusReportingSendTimerInterval = 1000 * 60 * 2; +} + +namespace OCC +{ +Q_LOGGING_CATEGORY(lcClientStatusReporting, "nextcloud.sync.clientstatusreporting", QtInfoMsg) + +ClientStatusReporting::ClientStatusReporting(Account *account, QObject *parent) + : _account(account) + , QObject(parent) +{ + init(); +} + +void ClientStatusReporting::init() +{ + if (_isInitialized) { + qCDebug(lcClientStatusReporting) << "Double call to init"; + return; + } + + for (int i = 0; i < ClientStatusReporting::Count; ++i) { + const auto statusString = statusStringFromNumber(static_cast(i)); + _statusNamesAndHashes[i] = {statusString, SyncJournalDb::getPHash(statusString)}; + } + + const auto databaseId = QStringLiteral("%1@%2").arg(_account->davUser(), _account->url().toString()); + const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); + + const QString journalPath = ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); + + _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); + _database.setDatabaseName(journalPath); + + if (!_database.open()) { + qCDebug(lcClientStatusReporting) << "Could not setup client reporting, database connection error."; + return; + } + + QSqlQuery query; + const auto prepareResult = query.prepare( + "CREATE TABLE IF NOT EXISTS clientstatusreporting(" + "nHash INTEGER(8) PRIMARY KEY," + "name VARCHAR(4096)," + "count INTEGER," + "lastOccurrence INTEGER(8))"); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReporting) << "Could not setup client clientstatusreporting table:" << query.lastError().text(); + return; + } + + if (!query.prepare("CREATE TABLE IF NOT EXISTS keyvalue(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key))") || !query.exec()) { + qCDebug(lcClientStatusReporting) << "Could not setup client keyvalue table:" << query.lastError().text(); + return; + } + + _clientStatusReportingSendTimer.setInterval(clientStatusReportingSendTimerInterval); + connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReporting::sendReportToServer); + _clientStatusReportingSendTimer.start(); + + _isInitialized = true; + + reportClientStatus(Status::DownloadError_ConflictCaseClash); + reportClientStatus(Status::DownloadError_ConflictInvalidCharacters); + reportClientStatus(Status::UploadError_ServerError); + reportClientStatus(Status::UploadError_ServerError); + setLastSentReportTimestamp(QDateTime::currentDateTime().toMSecsSinceEpoch()); + + auto records = getClientStatusReportingRecords(); + + auto resDelete = deleteClientStatusReportingRecords(); + + records = getClientStatusReportingRecords(); + + auto res = getLastSentReportTimestamp(); +} + +QVector ClientStatusReporting::getClientStatusReportingRecords() const +{ + QVector records; + + QMutexLocker locker(&_mutex); + + QSqlQuery query; + const auto prepareResult = query.prepare("SELECT * FROM clientstatusreporting"); + + if (!prepareResult || !query.exec()) { + const auto errorMessage = query.lastError().text(); + qCDebug(lcClientStatusReporting) << "Could not get records from clientstatusreporting:" << errorMessage; + return records; + } + + while (query.next()) { + ClientStatusReportingRecord record; + record._nameHash = query.value(query.record().indexOf("nHash")).toLongLong(); + record._name = query.value(query.record().indexOf("name")).toByteArray(); + record._numOccurences = query.value(query.record().indexOf("count")).toLongLong(); + record._lastOccurence = query.value(query.record().indexOf("lastOccurrence")).toLongLong(); + records.push_back(record); + } + return records; +} + +bool ClientStatusReporting::deleteClientStatusReportingRecords() +{ + QSqlQuery query; + const auto prepareResult = query.prepare("DELETE FROM clientstatusreporting"); + + if (!prepareResult || !query.exec()) { + const auto errorMessage = query.lastError().text(); + qCDebug(lcClientStatusReporting) << "Could not get records from clientstatusreporting:" << errorMessage; + return false; + } + return true; +} + +Result ClientStatusReporting::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) +{ + Q_ASSERT(record.isValid()); + if (!record.isValid()) { + qCWarning(lcClientStatusReporting) << "Failed to set ClientStatusReportingRecord"; + return {QStringLiteral("Invalid parameter")}; + } + + const auto recordCopy = record; + + QMutexLocker locker(&_mutex); + + QSqlQuery query; + + const auto prepareResult = query.prepare( + "INSERT OR REPLACE INTO clientstatusreporting (nHash, name, count, lastOccurrence) VALUES(:nHash, :name, :count, :lastOccurrence) ON CONFLICT(nHash) " + "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;"); + query.bindValue(":nHash", recordCopy._nameHash); + query.bindValue(":name", recordCopy._name); + query.bindValue(":count", 1); + query.bindValue(":lastOccurrence", recordCopy._lastOccurence); + + if (!prepareResult || !query.exec()) { + const auto errorMessage = query.lastError().text(); + qCDebug(lcClientStatusReporting) << "Could not report client status:" << errorMessage; + return errorMessage; + } + + return {}; +} + +void ClientStatusReporting::reportClientStatus(const Status status) +{ + if (!_isInitialized) { + qCWarning(lcClientStatusReporting) << "Could not report status. Status reporting is not initialized"; + return; + } + Q_ASSERT(status >= 0 && status < Count); + if (status < 0 || status >= Status::Count) { + qCWarning(lcClientStatusReporting) << "Trying to report invalid status:" << status; + return; + } + + ClientStatusReportingRecord record; + record._name = _statusNamesAndHashes[status].first; + record._nameHash = _statusNamesAndHashes[status].second; + record._lastOccurence = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); + const auto result = setClientStatusReportingRecord(record); + if (!result.isValid()) { + qCWarning(lcClientStatusReporting) << "Could not report client status:" << result.error(); + } +} + +void ClientStatusReporting::sendReportToServer() +{ + if (!_isInitialized) { + qCWarning(lcClientStatusReporting) << "Could not send report to server. Status reporting is not initialized"; + return; + } + + const auto lastSentReportTime = setLastSentReportTimestamp(0); + if (QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - lastSentReportTime < repordSendIntervalMs) { + return; + } + + const auto records = getClientStatusReportingRecords(); + if (!records.isEmpty()) { + // send to server -> + const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); + clientStatusReportingJob->setBody({}); + clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); + connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json) { + const QJsonObject data = json.object().value("ocs").toObject().value("data").toObject(); + slotSendReportToserverFinished(); + }); + clientStatusReportingJob->start(); + } +} + +void ClientStatusReporting::slotSendReportToserverFinished() +{ + if (!deleteClientStatusReportingRecords()) { + qCWarning(lcClientStatusReporting) << "Error deleting client status report."; + } + setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()); +} + +qulonglong ClientStatusReporting::getLastSentReportTimestamp() const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare("SELECT value FROM keyvalue WHERE key = (:key)"); + query.bindValue(":key", lastSentReportTimestamp); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; + return 0; + } + if (!query.next()) { + qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table:" << query.lastError().text(); + return 0; + } + + int valueIndex = query.record().indexOf("value"); + return query.value(valueIndex).toULongLong(); +} + +bool ClientStatusReporting::setLastSentReportTimestamp(const qulonglong timestamp) +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);"); + query.bindValue(":key", lastSentReportTimestamp); + query.bindValue(":value", timestamp); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReporting) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; + return false; + } + + return true; +} + +QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) +{ + Q_ASSERT(status >= 0 && status < Count); + if (status < 0 || status >= Status::Count) { + qCWarning(lcClientStatusReporting) << "Invalid status:" << status; + return {}; + } + + switch (status) { + case DownloadError_ConflictInvalidCharacters: + return QByteArrayLiteral("DownloadError.CONFLICT_INVALID_CHARACTERS"); + case DownloadError_ConflictCaseClash: + return QByteArrayLiteral("DownloadError.CONFLICT_CASECLASH"); + case UploadError_ServerError: + return QByteArrayLiteral("UploadError.SERVER_ERROR"); + case Count: + return {}; + }; + return {}; +} +} diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h new file mode 100644 index 0000000000000..303cc50791b15 --- /dev/null +++ b/src/libsync/clientstatusreporting.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "owncloudlib.h" +#include "accountfwd.h" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace OCC { + +class Account; +struct ClientStatusReportingRecord; + +class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject +{ + Q_OBJECT +public: + enum Status { + DownloadError_ConflictInvalidCharacters = 0, + DownloadError_ConflictCaseClash, + UploadError_ServerError, + Count, + }; + + explicit ClientStatusReporting(Account *account, QObject *parent = nullptr); + ~ClientStatusReporting() = default; + + void reportClientStatus(const Status status); + + void init(); + +private: + [[nodiscard]] Result setClientStatusReportingRecord(const ClientStatusReportingRecord &record); + [[nodiscard]] QVector getClientStatusReportingRecords() const; + [[nodiscard]] bool deleteClientStatusReportingRecords(); + [[nodiscard]] bool setLastSentReportTimestamp(const qulonglong timestamp); + [[nodiscard]] qulonglong getLastSentReportTimestamp() const; + +private slots: + void sendReportToServer(); + void slotSendReportToserverFinished(); + +private: + static QByteArray statusStringFromNumber(const Status status); + Account *_account = nullptr; + QHash> _statusNamesAndHashes; + QSqlDatabase _database; + bool _isInitialized = false; + QTimer _clientStatusReportingSendTimer; + mutable QRecursiveMutex _mutex; +}; +} diff --git a/src/libsync/ocsclientstatusreportingjob.cpp b/src/libsync/ocsclientstatusreportingjob.cpp new file mode 100644 index 0000000000000..ff9e0baf0668f --- /dev/null +++ b/src/libsync/ocsclientstatusreportingjob.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "ocsclientstatusreportingjob.h" +#include "networkjobs.h" +#include "account.h" + +#include +#include + +namespace OCC { + +OcsClientStatusReportingJob::OcsClientStatusReportingJob(AccountPtr account) + : OcsJob(account) +{ + setPath(QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); + connect(this, &OcsJob::jobFinished, this, &OcsClientStatusReportingJob::jobDone); +} + +void OcsClientStatusReportingJob::sendStatusReport(const QVariant &jsonData) +{ + setVerb("PUT"); + + addRawHeader("Ocs-APIREQUEST", "true"); + addRawHeader("Content-Type", "application/json"); + + const auto url = Utility::concatUrlPath(account()->url(), path()); + sendRequest(_verb, url, _request, QJsonDocument::fromVariant(jsonData.toMap()).toJson()); + AbstractNetworkJob::start(); +} + +void OcsClientStatusReportingJob::jobDone(QJsonDocument reply) +{ + emit jobFinished(reply, {}); +} +} diff --git a/src/libsync/ocsclientstatusreportingjob.h b/src/libsync/ocsclientstatusreportingjob.h new file mode 100644 index 0000000000000..c42cdc2bbe16c --- /dev/null +++ b/src/libsync/ocsclientstatusreportingjob.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "ocsjob.h" + +#include +#include + +namespace OCC { + +/** + * @brief The OcsClientStatusReportingJob class + * @ingroup gui + * + * Handle sending client status reports via OCS Diagnostics API. + */ +class OcsClientStatusReportingJob : public OcsJob +{ + Q_OBJECT +public: + explicit OcsClientStatusReportingJob(AccountPtr account); + void sendStatusReport(const QVariant &jsonData); + +signals: + void jobFinished(QJsonDocument reply, QVariant value); + +private slots: + void jobDone(QJsonDocument reply); +}; +} diff --git a/src/gui/ocsjob.cpp b/src/libsync/ocsjob.cpp similarity index 100% rename from src/gui/ocsjob.cpp rename to src/libsync/ocsjob.cpp diff --git a/src/gui/ocsjob.h b/src/libsync/ocsjob.h similarity index 99% rename from src/gui/ocsjob.h rename to src/libsync/ocsjob.h index 47e13e037b016..e22ac73abf5ff 100644 --- a/src/gui/ocsjob.h +++ b/src/libsync/ocsjob.h @@ -148,11 +148,13 @@ protected slots: private slots: bool finished() override; -private: +protected: QByteArray _verb; - QHash _params; - QVector _passStatusCodes; QNetworkRequest _request; + +private: + QVector _passStatusCodes; + QHash _params; }; } From 0ec761d373baab33fab2acb0a89e2dd8f8ea0b94 Mon Sep 17 00:00:00 2001 From: alex-z Date: Fri, 24 Nov 2023 15:47:51 +0100 Subject: [PATCH 2/9] Send status report. Improved logic and database columns. Signed-off-by: alex-z --- src/common/clientstatusreportingrecord.cpp | 2 +- src/common/clientstatusreportingrecord.h | 1 + src/libsync/clientstatusreporting.cpp | 62 +++++++++++++++++++++- src/libsync/clientstatusreporting.h | 2 + 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/common/clientstatusreportingrecord.cpp b/src/common/clientstatusreportingrecord.cpp index 0211b271ccbc8..95b0cda632d9f 100644 --- a/src/common/clientstatusreportingrecord.cpp +++ b/src/common/clientstatusreportingrecord.cpp @@ -19,6 +19,6 @@ namespace OCC bool ClientStatusReportingRecord::isValid() const { - return !_name.isEmpty() && _nameHash > 0 && _lastOccurence > 0; + return _status >= 0 && !_name.isEmpty() && _nameHash > 0 && _lastOccurence > 0; } } diff --git a/src/common/clientstatusreportingrecord.h b/src/common/clientstatusreportingrecord.h index a1aff6df09e23..ba0b05162938c 100644 --- a/src/common/clientstatusreportingrecord.h +++ b/src/common/clientstatusreportingrecord.h @@ -25,6 +25,7 @@ namespace OCC */ struct OCSYNC_EXPORT ClientStatusReportingRecord { QByteArray _name; + int _status = -1; quint64 _nameHash = 0; quint64 _numOccurences = 1; quint64 _lastOccurence = 0; diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp index d2985e5954eec..ec64866076ae0 100644 --- a/src/libsync/clientstatusreporting.cpp +++ b/src/libsync/clientstatusreporting.cpp @@ -66,6 +66,7 @@ void ClientStatusReporting::init() const auto prepareResult = query.prepare( "CREATE TABLE IF NOT EXISTS clientstatusreporting(" "nHash INTEGER(8) PRIMARY KEY," + "status INTEGER(8))" "name VARCHAR(4096)," "count INTEGER," "lastOccurrence INTEGER(8))"); @@ -118,6 +119,7 @@ QVector ClientStatusReporting::getClientStatusRepor while (query.next()) { ClientStatusReportingRecord record; record._nameHash = query.value(query.record().indexOf("nHash")).toLongLong(); + record._status = query.value(query.record().indexOf("status")).toLongLong(); record._name = query.value(query.record().indexOf("name")).toByteArray(); record._numOccurences = query.value(query.record().indexOf("count")).toLongLong(); record._lastOccurence = query.value(query.record().indexOf("lastOccurrence")).toLongLong(); @@ -154,10 +156,11 @@ Result ClientStatusReporting::setClientStatusReportingRecord(cons QSqlQuery query; const auto prepareResult = query.prepare( - "INSERT OR REPLACE INTO clientstatusreporting (nHash, name, count, lastOccurrence) VALUES(:nHash, :name, :count, :lastOccurrence) ON CONFLICT(nHash) " + "INSERT OR REPLACE INTO clientstatusreporting (nHash, name, count, lastOccurrence) VALUES(:nHash, :name, :status, :count, :lastOccurrence) ON CONFLICT(nHash) " "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;"); query.bindValue(":nHash", recordCopy._nameHash); query.bindValue(":name", recordCopy._name); + query.bindValue(":status", recordCopy._status); query.bindValue(":count", 1); query.bindValue(":lastOccurrence", recordCopy._lastOccurence); @@ -184,6 +187,7 @@ void ClientStatusReporting::reportClientStatus(const Status status) ClientStatusReportingRecord record; record._name = _statusNamesAndHashes[status].first; + record._status = status; record._nameHash = _statusNamesAndHashes[status].second; record._lastOccurence = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); const auto result = setClientStatusReportingRecord(record); @@ -207,8 +211,41 @@ void ClientStatusReporting::sendReportToServer() const auto records = getClientStatusReportingRecords(); if (!records.isEmpty()) { // send to server -> + + QVariantMap report; + + QVariantMap syncConflicts; + QVariantMap problems; + + for (const auto &record : records) { + const auto categoryKey = classifyStatus(static_cast(record._status)); + + if (categoryKey.isEmpty()) { + qCDebug(lcClientStatusReporting) << "Could not classify status:"; + continue; + } + + if (categoryKey == QStringLiteral("sync_conflicts")) { + const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); + syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; + syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = syncConflicts; + } else if (categoryKey == QStringLiteral("problems")) { + problems[record._name] = QVariantMap { + {QStringLiteral("count"), record._numOccurences}, + {QStringLiteral("oldest"), record._lastOccurence} + }; + report[categoryKey] = problems; + } + } + + if (report.isEmpty()) { + qCDebug(lcClientStatusReporting) << "Report is empty."; + return; + } + const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); - clientStatusReportingJob->setBody({}); + clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report)); clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json) { const QJsonObject data = json.object().value("ocs").toObject().value("data").toObject(); @@ -280,4 +317,25 @@ QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) }; return {}; } + +QString ClientStatusReporting::classifyStatus(const Status status) +{ + Q_ASSERT(status >= 0 && status < Count); + if (status < 0 || status >= Status::Count) { + qCWarning(lcClientStatusReporting) << "Invalid status:" << status; + return {}; + } + + switch (status) { + case DownloadError_ConflictInvalidCharacters: + return QStringLiteral("sync_conflicts"); + case DownloadError_ConflictCaseClash: + return QStringLiteral("sync_conflicts"); + case UploadError_ServerError: + return QByteArrayLiteral("problems"); + case Count: + return {}; + }; + return {}; +} } diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h index 303cc50791b15..a9f13d65fe7df 100644 --- a/src/libsync/clientstatusreporting.h +++ b/src/libsync/clientstatusreporting.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,7 @@ private slots: private: static QByteArray statusStringFromNumber(const Status status); + static QString classifyStatus(const Status status); Account *_account = nullptr; QHash> _statusNamesAndHashes; QSqlDatabase _database; From 994ecb3040692e55c0f2bfbeb38ad324db877e19 Mon Sep 17 00:00:00 2001 From: alex-z Date: Fri, 24 Nov 2023 16:00:40 +0100 Subject: [PATCH 3/9] Iteration. Use Capabilities to control Client Status Reporting availibility. Signed-off-by: alex-z --- src/libsync/account.cpp | 11 ++++++++++- src/libsync/capabilities.cpp | 9 +++++++++ src/libsync/capabilities.h | 2 ++ src/libsync/clientstatusreporting.cpp | 12 ++++++------ src/libsync/clientstatusreporting.h | 6 +++--- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 24a1562ac25bc..2c8a656262e55 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -284,7 +284,16 @@ void Account::setPushNotificationsReconnectInterval(int interval) void Account::trySetupClientStatusReporting() { - _clientStatusReporting.reset(new ClientStatusReporting(this)); + if (_capabilities.isClientStatusReportingEnabled()) { + if (!_clientStatusReporting) { + _clientStatusReporting.reset(new ClientStatusReporting(this)); + } + return; + } + + if (!_clientStatusReporting) { + _clientStatusReporting.reset(); + } } void Account::reportClientStatus(const int status) diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 386880b12ab03..14c51744a228b 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -198,6 +198,15 @@ bool Capabilities::hasActivities() const return _capabilities.contains("activity"); } +bool Capabilities::isClientStatusReportingEnabled() const +{ + if (!_capabilities.contains(QStringLiteral("security_guard"))) { + return false; + } + const auto securityGuardCaps = _capabilities[QStringLiteral("security_guard")].toMap(); + return securityGuardCaps.contains(QStringLiteral("diagnostics")) && securityGuardCaps[QStringLiteral("diagnostics")].toBool(); +} + QList Capabilities::supportedChecksumTypes() const { QList list; diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h index 0ebc25d80901d..9a7f587f00b99 100644 --- a/src/libsync/capabilities.h +++ b/src/libsync/capabilities.h @@ -97,6 +97,8 @@ class OWNCLOUDSYNC_EXPORT Capabilities /// return true if the activity app is enabled [[nodiscard]] bool hasActivities() const; + [[nodiscard]] bool isClientStatusReportingEnabled() const; + /** * Returns the checksum types the server understands. * diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp index ec64866076ae0..1f094f6a8c7c1 100644 --- a/src/libsync/clientstatusreporting.cpp +++ b/src/libsync/clientstatusreporting.cpp @@ -52,10 +52,10 @@ void ClientStatusReporting::init() const auto databaseId = QStringLiteral("%1@%2").arg(_account->davUser(), _account->url().toString()); const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); - const QString journalPath = ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); + const QString dbPath = ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); - _database.setDatabaseName(journalPath); + _database.setDatabaseName(dbPath); if (!_database.open()) { qCDebug(lcClientStatusReporting) << "Could not setup client reporting, database connection error."; @@ -66,7 +66,7 @@ void ClientStatusReporting::init() const auto prepareResult = query.prepare( "CREATE TABLE IF NOT EXISTS clientstatusreporting(" "nHash INTEGER(8) PRIMARY KEY," - "status INTEGER(8))" + "status INTEGER(8)," "name VARCHAR(4096)," "count INTEGER," "lastOccurrence INTEGER(8))"); @@ -94,7 +94,7 @@ void ClientStatusReporting::init() auto records = getClientStatusReportingRecords(); - auto resDelete = deleteClientStatusReportingRecords(); + // auto resDelete = deleteClientStatusReportingRecords(); records = getClientStatusReportingRecords(); @@ -156,7 +156,7 @@ Result ClientStatusReporting::setClientStatusReportingRecord(cons QSqlQuery query; const auto prepareResult = query.prepare( - "INSERT OR REPLACE INTO clientstatusreporting (nHash, name, count, lastOccurrence) VALUES(:nHash, :name, :status, :count, :lastOccurrence) ON CONFLICT(nHash) " + "INSERT OR REPLACE INTO clientstatusreporting (nHash, name, status, count, lastOccurrence) VALUES(:nHash, :name, :status, :count, :lastOccurrence) ON CONFLICT(nHash) " "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;"); query.bindValue(":nHash", recordCopy._nameHash); query.bindValue(":name", recordCopy._name); @@ -203,7 +203,7 @@ void ClientStatusReporting::sendReportToServer() return; } - const auto lastSentReportTime = setLastSentReportTimestamp(0); + const auto lastSentReportTime = getLastSentReportTimestamp(); if (QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - lastSentReportTime < repordSendIntervalMs) { return; } diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h index a9f13d65fe7df..a430d08119ec4 100644 --- a/src/libsync/clientstatusreporting.h +++ b/src/libsync/clientstatusreporting.h @@ -45,11 +45,10 @@ class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject explicit ClientStatusReporting(Account *account, QObject *parent = nullptr); ~ClientStatusReporting() = default; - void reportClientStatus(const Status status); - +private: void init(); + void reportClientStatus(const Status status); -private: [[nodiscard]] Result setClientStatusReportingRecord(const ClientStatusReportingRecord &record); [[nodiscard]] QVector getClientStatusReportingRecords() const; [[nodiscard]] bool deleteClientStatusReportingRecords(); @@ -69,5 +68,6 @@ private slots: bool _isInitialized = false; QTimer _clientStatusReportingSendTimer; mutable QRecursiveMutex _mutex; + friend class Account; }; } From 1118496b905b7d931458359a076b649a6a6635af Mon Sep 17 00:00:00 2001 From: alex-z Date: Fri, 24 Nov 2023 16:26:20 +0100 Subject: [PATCH 4/9] Iteration. Properly process server reply. Signed-off-by: alex-z --- src/libsync/account.cpp | 5 +- src/libsync/account.h | 4 +- src/libsync/clientstatusreporting.cpp | 126 +++++++++++++++++--------- src/libsync/clientstatusreporting.h | 12 ++- src/libsync/networkjobs.cpp | 2 +- src/libsync/owncloudpropagator.cpp | 26 ++++++ src/libsync/propagatedownload.cpp | 3 +- src/libsync/syncengine.cpp | 3 + 8 files changed, 128 insertions(+), 53 deletions(-) diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 2c8a656262e55..876efca66652f 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -17,7 +17,6 @@ #include "accountfwd.h" #include "capabilities.h" #include "clientsideencryptionjobs.h" -#include "clientstatusreporting.h" #include "configfile.h" #include "cookiejar.h" #include "creds/abstractcredentials.h" @@ -296,10 +295,10 @@ void Account::trySetupClientStatusReporting() } } -void Account::reportClientStatus(const int status) +void Account::reportClientStatus(const ClientStatusReporting::Status status) { if (_clientStatusReporting) { - _clientStatusReporting->reportClientStatus(static_cast(status)); + _clientStatusReporting->reportClientStatus(status); } } diff --git a/src/libsync/account.h b/src/libsync/account.h index 98aa1cb021f95..1c51bb7a0c6b5 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -33,6 +33,7 @@ #include "capabilities.h" #include "clientsideencryption.h" +#include "clientstatusreporting.h" #include "common/utility.h" #include "syncfileitem.h" @@ -55,7 +56,6 @@ class AbstractCredentials; class Account; using AccountPtr = QSharedPointer; class AccessManager; -class ClientStatusReporting; class SimpleNetworkJob; class PushNotifications; class UserStatusConnector; @@ -308,7 +308,7 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject void trySetupClientStatusReporting(); - void reportClientStatus(const int status); + void reportClientStatus(const ClientStatusReporting::Status status); [[nodiscard]] std::shared_ptr userStatusConnector() const; diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp index 1f094f6a8c7c1..4f3f127af41e3 100644 --- a/src/libsync/clientstatusreporting.cpp +++ b/src/libsync/clientstatusreporting.cpp @@ -22,8 +22,10 @@ namespace { constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; -constexpr auto repordSendIntervalMs = 24 * 60 * 60 * 1000; -constexpr int clientStatusReportingSendTimerInterval = 1000 * 60 * 2; +//constexpr auto repordSendIntervalMs = 24 * 60 * 60 * 1000; +//constexpr int clientStatusReportingSendTimerInterval = 1000 * 60 * 2; +constexpr auto repordSendIntervalMs = 2000; +constexpr int clientStatusReportingSendTimerInterval = 5000; } namespace OCC @@ -209,50 +211,55 @@ void ClientStatusReporting::sendReportToServer() } const auto records = getClientStatusReportingRecords(); - if (!records.isEmpty()) { - // send to server -> - - QVariantMap report; - - QVariantMap syncConflicts; - QVariantMap problems; - - for (const auto &record : records) { - const auto categoryKey = classifyStatus(static_cast(record._status)); - - if (categoryKey.isEmpty()) { - qCDebug(lcClientStatusReporting) << "Could not classify status:"; - continue; - } - - if (categoryKey == QStringLiteral("sync_conflicts")) { - const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); - syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; - syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; - report[categoryKey] = syncConflicts; - } else if (categoryKey == QStringLiteral("problems")) { - problems[record._name] = QVariantMap { - {QStringLiteral("count"), record._numOccurences}, - {QStringLiteral("oldest"), record._lastOccurence} - }; - report[categoryKey] = problems; - } + if (records.isEmpty()) { + return; + } + + QVariantMap report; + + report[QStringLiteral("sync_conflicts")] = QVariantMap{}; + report[QStringLiteral("problems")] = QVariantMap{}; + report[QStringLiteral("virus_detected")] = QVariantMap{}; + report[QStringLiteral ("e2e_errors")] = QVariantMap{}; + + QVariantMap syncConflicts; + QVariantMap problems; + + for (const auto &record : records) { + const auto categoryKey = classifyStatus(static_cast(record._status)); + + if (categoryKey.isEmpty()) { + qCDebug(lcClientStatusReporting) << "Could not classify status:"; + continue; } - if (report.isEmpty()) { - qCDebug(lcClientStatusReporting) << "Report is empty."; - return; + if (categoryKey == QStringLiteral("sync_conflicts")) { + const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); + syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; + syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = syncConflicts; + } else if (categoryKey == QStringLiteral("problems")) { + problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}}; + report[categoryKey] = problems; } + } - const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); - clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report)); - clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); - connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json) { - const QJsonObject data = json.object().value("ocs").toObject().value("data").toObject(); - slotSendReportToserverFinished(); - }); - clientStatusReportingJob->start(); + if (report.isEmpty()) { + qCDebug(lcClientStatusReporting) << "Failed to generate report. Report is empty."; + return; } + + const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); + clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report)); + clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); + connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json, int statusCode) { + if (statusCode == 200 || statusCode == 204) { + const auto data = json.object().value("ocs").toObject().value("data").toObject(); + const auto dataMap = data.toVariantMap(); + slotSendReportToserverFinished(); + } + }); + clientStatusReportingJob->start(); } void ClientStatusReporting::slotSendReportToserverFinished() @@ -306,10 +313,30 @@ QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) } switch (status) { - case DownloadError_ConflictInvalidCharacters: - return QByteArrayLiteral("DownloadError.CONFLICT_INVALID_CHARACTERS"); + case DownloadError_Cannot_Create_File: + return QByteArrayLiteral("DownloadError.CANNOT_CREATE_FILE"); + case DownloadError_Conflict: + return QByteArrayLiteral("DownloadError.CONFLICT"); case DownloadError_ConflictCaseClash: return QByteArrayLiteral("DownloadError.CONFLICT_CASECLASH"); + case DownloadError_ConflictInvalidCharacters: + return QByteArrayLiteral("DownloadError.CONFLICT_INVALID_CHARACTERS"); + case DownloadError_No_Free_Space: + return QByteArrayLiteral("DownloadError.NO_FREE_SPACE"); + case DownloadError_ServerError: + return QByteArrayLiteral("DownloadError.SERVER_ERROR"); + case DownloadError_Virtual_File_Hydration_Failure: + return QByteArrayLiteral("DownloadError.VIRTUAL_FILE_HYDRATION_FAILURE "); + case UploadError_Conflict: + return QByteArrayLiteral("UploadError.CONFLICT"); + case UploadError_ConflictCaseClash: + return QByteArrayLiteral("UploadError.CONFLICT_CASECLASH"); + case UploadError_ConflictInvalidCharacters: + return QByteArrayLiteral("UploadError.CONFLICT_INVALID_CHARACTERS"); + case UploadError_No_Free_Space: + return QByteArrayLiteral("UploadError.NO_FREE_SPACE"); + case UploadError_No_Write_Permissions: + return QByteArrayLiteral("UploadError.NO_WRITE_PERMISSIONS"); case UploadError_ServerError: return QByteArrayLiteral("UploadError.SERVER_ERROR"); case Count: @@ -327,10 +354,19 @@ QString ClientStatusReporting::classifyStatus(const Status status) } switch (status) { - case DownloadError_ConflictInvalidCharacters: - return QStringLiteral("sync_conflicts"); + case DownloadError_Conflict: case DownloadError_ConflictCaseClash: + case DownloadError_ConflictInvalidCharacters: + case UploadError_Conflict: + case UploadError_ConflictCaseClash: + case UploadError_ConflictInvalidCharacters: return QStringLiteral("sync_conflicts"); + case DownloadError_Cannot_Create_File: + case DownloadError_No_Free_Space: + case DownloadError_ServerError: + case DownloadError_Virtual_File_Hydration_Failure: + case UploadError_No_Free_Space: + case UploadError_No_Write_Permissions: case UploadError_ServerError: return QByteArrayLiteral("problems"); case Count: diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h index a430d08119ec4..cbdf5bd115344 100644 --- a/src/libsync/clientstatusreporting.h +++ b/src/libsync/clientstatusreporting.h @@ -36,8 +36,18 @@ class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject Q_OBJECT public: enum Status { - DownloadError_ConflictInvalidCharacters = 0, + DownloadError_Cannot_Create_File = 0, + DownloadError_Conflict, DownloadError_ConflictCaseClash, + DownloadError_ConflictInvalidCharacters, + DownloadError_No_Free_Space, + DownloadError_ServerError, + DownloadError_Virtual_File_Hydration_Failure, + UploadError_Conflict, + UploadError_ConflictCaseClash, + UploadError_ConflictInvalidCharacters, + UploadError_No_Free_Space, + UploadError_No_Write_Permissions, UploadError_ServerError, Count, }; diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp index 3c784675feb61..5b2722848ab42 100644 --- a/src/libsync/networkjobs.cpp +++ b/src/libsync/networkjobs.cpp @@ -943,7 +943,7 @@ JsonApiJob::JsonApiJob(const AccountPtr &account, const QString &path, QObject * void JsonApiJob::setBody(const QJsonDocument &body) { - SimpleApiJob::setBody(body.toJson()); + SimpleApiJob::setBody(body.toJson(QJsonDocument::JsonFormat::Compact)); qCDebug(lcJsonApiJob) << "Set body for request:" << SimpleApiJob::body(); if (!SimpleApiJob::body().isEmpty()) { request().setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 4745b4f846fbe..839d3267b52f0 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -229,6 +229,30 @@ void PropagateItemJob::done(const SyncFileItem::Status statusArg, const QString _item->_status = statusArg; + if (_item->_status == SyncFileItem::Status::Conflict) { + if (_item->_direction == SyncFileItem::Direction::Up) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Conflict); + } else { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); + } + } else if (_item->_status == SyncFileItem::Status::FileNameClash) { + if (_item->_direction == SyncFileItem::Direction::Up) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); + } else { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); + } + } else if (_item->_status == SyncFileItem::Status::FileNameInvalidOnServer) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); + } else if (_item->_status == SyncFileItem::Status::FileNameInvalid) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); + } else if (_item->_httpErrorCode != 0 && _item->_httpErrorCode != 200) { + if (_item->_direction == SyncFileItem::Up) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError); + } else { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ServerError); + } + } + if (_item->_isRestoration) { if (_item->_status == SyncFileItem::Success || _item->_status == SyncFileItem::Conflict) { @@ -917,6 +941,7 @@ bool OwncloudPropagator::createConflict(const SyncFileItemPtr &item, } _journal->setConflictRecord(conflictRecord); + account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); // Create a new upload job if the new conflict file should be uploaded if (account()->capabilities().uploadConflictFiles()) { @@ -989,6 +1014,7 @@ OCC::Optional OwncloudPropagator::createCaseClashConflict(const SyncFil } _journal->setCaseConflictRecord(conflictRecord); + account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictCaseClash); // Need a new sync to detect the created copy of the conflicting file _anotherSyncNeeded = true; diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index 6d7d5e2ebee0b..ecf74e78d0007 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -1280,8 +1280,9 @@ void PropagateDownloadFile::downloadFinished() // Maybe what we downloaded was a conflict file? If so, set a conflict record. // (the data was prepared in slotGetFinished above) - if (_conflictRecord.isValid()) + if (_conflictRecord.isValid()) { propagator()->_journal->setConflictRecord(_conflictRecord); + } if (vfs && vfs->mode() == Vfs::WithSuffix) { // If the virtual file used to have a different name and db diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index e4e76a7ff79b7..91ffcaea4dfb3 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -314,6 +314,7 @@ void SyncEngine::conflictRecordMaintenance() } _journal->setConflictRecord(record); + account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); } } } @@ -551,6 +552,7 @@ void SyncEngine::startSync() .arg( Utility::octetsToString(freeBytes), Utility::octetsToString(minFree)), ErrorCategory::GenericError); + account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_No_Free_Space); finalize(false); return; } else { @@ -1270,6 +1272,7 @@ void SyncEngine::slotInsufficientLocalStorage() void SyncEngine::slotInsufficientRemoteStorage() { auto msg = tr("There is insufficient space available on the server for some uploads."); + account()->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Free_Space); if (_uniqueErrors.contains(msg)) return; From ea58bf45154251800f85ed8fe1078aa52326df7a Mon Sep 17 00:00:00 2001 From: alex-z Date: Mon, 27 Nov 2023 18:47:17 +0100 Subject: [PATCH 5/9] Generate client status report records when errors happen. Signed-off-by: alex-z --- src/libsync/clientstatusreporting.cpp | 153 +++++++++++++------------- src/libsync/clientstatusreporting.h | 45 +++++--- src/libsync/discovery.cpp | 2 + src/libsync/owncloudpropagator.cpp | 51 +++++---- src/libsync/owncloudpropagator.h | 2 + src/libsync/propagatedownload.cpp | 2 + src/libsync/syncengine.cpp | 4 +- src/libsync/vfs/cfapi/vfs_cfapi.cpp | 1 + test/CMakeLists.txt | 1 + test/testclientstatusreporting.cpp | 131 ++++++++++++++++++++++ 10 files changed, 275 insertions(+), 117 deletions(-) create mode 100644 test/testclientstatusreporting.cpp diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp index 4f3f127af41e3..3311141e869e5 100644 --- a/src/libsync/clientstatusreporting.cpp +++ b/src/libsync/clientstatusreporting.cpp @@ -22,10 +22,6 @@ namespace { constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; -//constexpr auto repordSendIntervalMs = 24 * 60 * 60 * 1000; -//constexpr int clientStatusReportingSendTimerInterval = 1000 * 60 * 2; -constexpr auto repordSendIntervalMs = 2000; -constexpr int clientStatusReportingSendTimerInterval = 5000; } namespace OCC @@ -39,6 +35,13 @@ ClientStatusReporting::ClientStatusReporting(Account *account, QObject *parent) init(); } +ClientStatusReporting::~ClientStatusReporting() +{ + if (_database.isOpen()) { + _database.close(); + } +} + void ClientStatusReporting::init() { if (_isInitialized) { @@ -51,10 +54,7 @@ void ClientStatusReporting::init() _statusNamesAndHashes[i] = {statusString, SyncJournalDb::getPHash(statusString)}; } - const auto databaseId = QStringLiteral("%1@%2").arg(_account->davUser(), _account->url().toString()); - const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); - - const QString dbPath = ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); + const auto dbPath = makeDbPath(); _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); _database.setDatabaseName(dbPath); @@ -82,25 +82,11 @@ void ClientStatusReporting::init() return; } - _clientStatusReportingSendTimer.setInterval(clientStatusReportingSendTimerInterval); + _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval); connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReporting::sendReportToServer); _clientStatusReportingSendTimer.start(); _isInitialized = true; - - reportClientStatus(Status::DownloadError_ConflictCaseClash); - reportClientStatus(Status::DownloadError_ConflictInvalidCharacters); - reportClientStatus(Status::UploadError_ServerError); - reportClientStatus(Status::UploadError_ServerError); - setLastSentReportTimestamp(QDateTime::currentDateTime().toMSecsSinceEpoch()); - - auto records = getClientStatusReportingRecords(); - - // auto resDelete = deleteClientStatusReportingRecords(); - - records = getClientStatusReportingRecords(); - - auto res = getLastSentReportTimestamp(); } QVector ClientStatusReporting::getClientStatusReportingRecords() const @@ -147,7 +133,7 @@ Result ClientStatusReporting::setClientStatusReportingRecord(cons { Q_ASSERT(record.isValid()); if (!record.isValid()) { - qCWarning(lcClientStatusReporting) << "Failed to set ClientStatusReportingRecord"; + qCDebug(lcClientStatusReporting) << "Failed to set ClientStatusReportingRecord"; return {QStringLiteral("Invalid parameter")}; } @@ -183,7 +169,7 @@ void ClientStatusReporting::reportClientStatus(const Status status) } Q_ASSERT(status >= 0 && status < Count); if (status < 0 || status >= Status::Count) { - qCWarning(lcClientStatusReporting) << "Trying to report invalid status:" << status; + qCDebug(lcClientStatusReporting) << "Trying to report invalid status:" << status; return; } @@ -194,7 +180,7 @@ void ClientStatusReporting::reportClientStatus(const Status status) record._lastOccurence = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); const auto result = setClientStatusReportingRecord(record); if (!result.isValid()) { - qCWarning(lcClientStatusReporting) << "Could not report client status:" << result.error(); + qCDebug(lcClientStatusReporting) << "Could not report client status:" << result.error(); } } @@ -210,40 +196,7 @@ void ClientStatusReporting::sendReportToServer() return; } - const auto records = getClientStatusReportingRecords(); - if (records.isEmpty()) { - return; - } - - QVariantMap report; - - report[QStringLiteral("sync_conflicts")] = QVariantMap{}; - report[QStringLiteral("problems")] = QVariantMap{}; - report[QStringLiteral("virus_detected")] = QVariantMap{}; - report[QStringLiteral ("e2e_errors")] = QVariantMap{}; - - QVariantMap syncConflicts; - QVariantMap problems; - - for (const auto &record : records) { - const auto categoryKey = classifyStatus(static_cast(record._status)); - - if (categoryKey.isEmpty()) { - qCDebug(lcClientStatusReporting) << "Could not classify status:"; - continue; - } - - if (categoryKey == QStringLiteral("sync_conflicts")) { - const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); - syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; - syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; - report[categoryKey] = syncConflicts; - } else if (categoryKey == QStringLiteral("problems")) { - problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}}; - report[categoryKey] = problems; - } - } - + const auto report = prepareReport(); if (report.isEmpty()) { qCDebug(lcClientStatusReporting) << "Failed to generate report. Report is empty."; return; @@ -253,23 +206,38 @@ void ClientStatusReporting::sendReportToServer() clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report)); clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json, int statusCode) { - if (statusCode == 200 || statusCode == 204) { - const auto data = json.object().value("ocs").toObject().value("data").toObject(); - const auto dataMap = data.toVariantMap(); - slotSendReportToserverFinished(); + if (statusCode == 0 || statusCode == 200 || statusCode == 201 || statusCode == 204) { + const auto metaFromJson = json.object().value("ocs").toObject().value("meta").toObject(); + const auto codeFromJson = metaFromJson.value("statuscode").toInt(); + if (codeFromJson == 0 || codeFromJson == 200 || codeFromJson == 201 || codeFromJson == 204) { + reportToServerSentSuccessfully(); + return; + } + qCDebug(lcClientStatusReporting) << "Received error when sending client report statusCode:" << statusCode << "codeFromJson:" << codeFromJson; } }); clientStatusReportingJob->start(); } -void ClientStatusReporting::slotSendReportToserverFinished() +void ClientStatusReporting::reportToServerSentSuccessfully() { if (!deleteClientStatusReportingRecords()) { - qCWarning(lcClientStatusReporting) << "Error deleting client status report."; + qCDebug(lcClientStatusReporting) << "Error deleting client status report."; } setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()); } +QString ClientStatusReporting::makeDbPath() const +{ + if (!dbPathForTesting.isEmpty()) { + return dbPathForTesting; + } + const auto databaseId = QStringLiteral("%1@%2").arg(_account->davUser(), _account->url().toString()); + const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); + + return ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); +} + qulonglong ClientStatusReporting::getLastSentReportTimestamp() const { QMutexLocker locker(&_mutex); @@ -289,7 +257,44 @@ qulonglong ClientStatusReporting::getLastSentReportTimestamp() const return query.value(valueIndex).toULongLong(); } -bool ClientStatusReporting::setLastSentReportTimestamp(const qulonglong timestamp) +QVariantMap ClientStatusReporting::prepareReport() const +{ + const auto records = getClientStatusReportingRecords(); + if (records.isEmpty()) { + return {}; + } + + QVariantMap report; + report[QStringLiteral("sync_conflicts")] = QVariantMap{}; + report[QStringLiteral("problems")] = QVariantMap{}; + report[QStringLiteral("virus_detected")] = QVariantMap{}; + report[QStringLiteral("e2e_errors")] = QVariantMap{}; + + QVariantMap syncConflicts; + QVariantMap problems; + + for (const auto &record : records) { + const auto categoryKey = classifyStatus(static_cast(record._status)); + + if (categoryKey.isEmpty()) { + qCDebug(lcClientStatusReporting) << "Could not classify status:"; + continue; + } + + if (categoryKey == QStringLiteral("sync_conflicts")) { + const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); + syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; + syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = syncConflicts; + } else if (categoryKey == QStringLiteral("problems")) { + problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}}; + report[categoryKey] = problems; + } + } + return report; +} + +void ClientStatusReporting::setLastSentReportTimestamp(const qulonglong timestamp) { QMutexLocker locker(&_mutex); QSqlQuery query; @@ -298,17 +303,15 @@ bool ClientStatusReporting::setLastSentReportTimestamp(const qulonglong timestam query.bindValue(":value", timestamp); if (!prepareResult || !query.exec()) { qCDebug(lcClientStatusReporting) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; - return false; + return; } - - return true; } QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) { Q_ASSERT(status >= 0 && status < Count); if (status < 0 || status >= Status::Count) { - qCWarning(lcClientStatusReporting) << "Invalid status:" << status; + qCDebug(lcClientStatusReporting) << "Invalid status:" << status; return {}; } @@ -328,8 +331,6 @@ QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) case DownloadError_Virtual_File_Hydration_Failure: return QByteArrayLiteral("DownloadError.VIRTUAL_FILE_HYDRATION_FAILURE "); case UploadError_Conflict: - return QByteArrayLiteral("UploadError.CONFLICT"); - case UploadError_ConflictCaseClash: return QByteArrayLiteral("UploadError.CONFLICT_CASECLASH"); case UploadError_ConflictInvalidCharacters: return QByteArrayLiteral("UploadError.CONFLICT_INVALID_CHARACTERS"); @@ -349,7 +350,7 @@ QString ClientStatusReporting::classifyStatus(const Status status) { Q_ASSERT(status >= 0 && status < Count); if (status < 0 || status >= Status::Count) { - qCWarning(lcClientStatusReporting) << "Invalid status:" << status; + qCDebug(lcClientStatusReporting) << "Invalid status:" << status; return {}; } @@ -358,7 +359,6 @@ QString ClientStatusReporting::classifyStatus(const Status status) case DownloadError_ConflictCaseClash: case DownloadError_ConflictInvalidCharacters: case UploadError_Conflict: - case UploadError_ConflictCaseClash: case UploadError_ConflictInvalidCharacters: return QStringLiteral("sync_conflicts"); case DownloadError_Cannot_Create_File: @@ -374,4 +374,7 @@ QString ClientStatusReporting::classifyStatus(const Status status) }; return {}; } +int ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000 * 60 * 2; // check if the time has come, every 2 minutes +int ClientStatusReporting::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours +QString ClientStatusReporting::dbPathForTesting; } diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h index cbdf5bd115344..70e232086a72b 100644 --- a/src/libsync/clientstatusreporting.h +++ b/src/libsync/clientstatusreporting.h @@ -26,6 +26,8 @@ #include #include +class TestClientStatusReporting; + namespace OCC { class Account; @@ -36,24 +38,23 @@ class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject Q_OBJECT public: enum Status { - DownloadError_Cannot_Create_File = 0, - DownloadError_Conflict, - DownloadError_ConflictCaseClash, - DownloadError_ConflictInvalidCharacters, - DownloadError_No_Free_Space, - DownloadError_ServerError, - DownloadError_Virtual_File_Hydration_Failure, - UploadError_Conflict, - UploadError_ConflictCaseClash, - UploadError_ConflictInvalidCharacters, - UploadError_No_Free_Space, - UploadError_No_Write_Permissions, - UploadError_ServerError, - Count, + DownloadError_Cannot_Create_File = 100, + DownloadError_Conflict = 101, + DownloadError_ConflictCaseClash = 102, + DownloadError_ConflictInvalidCharacters = 103, + DownloadError_No_Free_Space = 104, + DownloadError_ServerError = 105, + DownloadError_Virtual_File_Hydration_Failure = 106, + UploadError_Conflict = 107, + UploadError_ConflictInvalidCharacters = 108, + UploadError_No_Free_Space = 109, + UploadError_No_Write_Permissions = 110, + UploadError_ServerError = 111, + Count = UploadError_ServerError + 1, }; explicit ClientStatusReporting(Account *account, QObject *parent = nullptr); - ~ClientStatusReporting() = default; + ~ClientStatusReporting(); private: void init(); @@ -62,22 +63,32 @@ class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject [[nodiscard]] Result setClientStatusReportingRecord(const ClientStatusReportingRecord &record); [[nodiscard]] QVector getClientStatusReportingRecords() const; [[nodiscard]] bool deleteClientStatusReportingRecords(); - [[nodiscard]] bool setLastSentReportTimestamp(const qulonglong timestamp); + void setLastSentReportTimestamp(const qulonglong timestamp); [[nodiscard]] qulonglong getLastSentReportTimestamp() const; + [[nodiscard]] QVariantMap prepareReport() const; + void reportToServerSentSuccessfully(); + [[nodiscard]] QString makeDbPath() const; private slots: void sendReportToServer(); - void slotSendReportToserverFinished(); private: static QByteArray statusStringFromNumber(const Status status); static QString classifyStatus(const Status status); + + static int clientStatusReportingTrySendTimerInterval; + static int repordSendIntervalMs; + + static QString dbPathForTesting; + Account *_account = nullptr; QHash> _statusNamesAndHashes; QSqlDatabase _database; bool _isInitialized = false; QTimer _clientStatusReportingSendTimer; mutable QRecursiveMutex _mutex; + friend class Account; + friend class TestClientStatusReporting; }; } diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index f11b9ab907fef..c316dd518aa40 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -1709,11 +1709,13 @@ bool ProcessDirectoryJob::checkPermissions(const OCC::SyncFileItemPtr &item) // No permissions set return true; } else if (item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) { + _discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions); qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file; item->_instruction = CSYNC_INSTRUCTION_ERROR; item->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder"); return false; } else if (!item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) { + _discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions); qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file; item->_instruction = CSYNC_INSTRUCTION_ERROR; item->_errorString = tr("Not allowed because you don't have permission to add files in that folder"); diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 839d3267b52f0..b71f7261eac06 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -229,29 +229,7 @@ void PropagateItemJob::done(const SyncFileItem::Status statusArg, const QString _item->_status = statusArg; - if (_item->_status == SyncFileItem::Status::Conflict) { - if (_item->_direction == SyncFileItem::Direction::Up) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Conflict); - } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); - } - } else if (_item->_status == SyncFileItem::Status::FileNameClash) { - if (_item->_direction == SyncFileItem::Direction::Up) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); - } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); - } - } else if (_item->_status == SyncFileItem::Status::FileNameInvalidOnServer) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); - } else if (_item->_status == SyncFileItem::Status::FileNameInvalid) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); - } else if (_item->_httpErrorCode != 0 && _item->_httpErrorCode != 200) { - if (_item->_direction == SyncFileItem::Up) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError); - } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ServerError); - } - } + reportClientStatuses(); if (_item->_isRestoration) { if (_item->_status == SyncFileItem::Success @@ -360,6 +338,33 @@ bool PropagateItemJob::hasEncryptedAncestor() const return false; } +void PropagateItemJob::reportClientStatuses() +{ + if (_item->_status == SyncFileItem::Status::Conflict) { + if (_item->_direction == SyncFileItem::Direction::Up) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Conflict); + } else { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); + } + } else if (_item->_status == SyncFileItem::Status::FileNameClash) { + if (_item->_direction == SyncFileItem::Direction::Up) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); + } else { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); + } + } else if (_item->_status == SyncFileItem::Status::FileNameInvalidOnServer) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); + } else if (_item->_status == SyncFileItem::Status::FileNameInvalid) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); + } else if (_item->_httpErrorCode != 0 && _item->_httpErrorCode != 200 && _item->_httpErrorCode != 201 && _item->_httpErrorCode != 204) { + if (_item->_direction == SyncFileItem::Up) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError); + } else { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ServerError); + } + } +} + // ================================================================================ PropagateItemJob *OwncloudPropagator::createJob(const SyncFileItemPtr &item) diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h index 440b98551e51f..06e840fb106fb 100644 --- a/src/libsync/owncloudpropagator.h +++ b/src/libsync/owncloudpropagator.h @@ -194,6 +194,8 @@ protected slots: void slotRestoreJobFinished(SyncFileItem::Status status); private: + void reportClientStatuses(); + QScopedPointer _restoreJob; JobParallelism _parallelism = FullParallelism; diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index ecf74e78d0007..456e079a568b6 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -675,6 +675,7 @@ void PropagateDownloadFile::startDownload() if (_tmpFile.exists()) FileSystem::setFileReadOnly(_tmpFile.fileName(), false); if (!_tmpFile.open(QIODevice::Append | QIODevice::Unbuffered)) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File); qCWarning(lcPropagateDownload) << "could not open temporary file" << _tmpFile.fileName(); done(SyncFileItem::NormalError, _tmpFile.errorString(), ErrorCategory::GenericError); return; @@ -1259,6 +1260,7 @@ void PropagateDownloadFile::downloadFinished() emit propagator()->touchedFile(filename); // The fileChanged() check is done above to generate better error messages. if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), filename, &error)) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File); qCWarning(lcPropagateDownload) << QString("Rename failed: %1 => %2").arg(_tmpFile.fileName()).arg(filename); // If the file is locked, we want to retry this sync when it // becomes available again, otherwise try again directly diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 91ffcaea4dfb3..ed2e09d028dd9 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -552,7 +552,6 @@ void SyncEngine::startSync() .arg( Utility::octetsToString(freeBytes), Utility::octetsToString(minFree)), ErrorCategory::GenericError); - account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_No_Free_Space); finalize(false); return; } else { @@ -1263,6 +1262,7 @@ void SyncEngine::slotSummaryError(const QString &message) void SyncEngine::slotInsufficientLocalStorage() { + account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_No_Free_Space); slotSummaryError( tr("Disk space is low: Downloads that would reduce free space " "below %1 were skipped.") @@ -1271,8 +1271,8 @@ void SyncEngine::slotInsufficientLocalStorage() void SyncEngine::slotInsufficientRemoteStorage() { - auto msg = tr("There is insufficient space available on the server for some uploads."); account()->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Free_Space); + auto msg = tr("There is insufficient space available on the server for some uploads."); if (_uniqueErrors.contains(msg)) return; diff --git a/src/libsync/vfs/cfapi/vfs_cfapi.cpp b/src/libsync/vfs/cfapi/vfs_cfapi.cpp index e7905fe6ca7c0..4a5a958eeabc5 100644 --- a/src/libsync/vfs/cfapi/vfs_cfapi.cpp +++ b/src/libsync/vfs/cfapi/vfs_cfapi.cpp @@ -464,6 +464,7 @@ void VfsCfApi::onHydrationJobFinished(HydrationJob *job) qCInfo(lcCfApi) << "Hydration job finished" << job->requestId() << job->folderPath() << job->status(); emit hydrationRequestFinished(job->requestId()); if (!job->errorString().isEmpty()) { + params().account->reportClientStatus(ClientStatusReporting::Status::DownloadError_Virtual_File_Hydration_Failure); emit failureHydrating(job->errorCode(), job->statusCode(), job->errorString(), job->folderPath()); } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 829b1d36c0188..44396f29a6caa 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -75,6 +75,7 @@ nextcloud_add_test(SecureFileDrop) nextcloud_add_test(FileTagModel) nextcloud_add_test(SyncConflictsModel) nextcloud_add_test(DateFieldBackend) +nextcloud_add_test(ClientStatusReporting) target_link_libraries(SecureFileDropTest PRIVATE Nextcloud::sync) configure_file(fake2eelocksucceeded.json "${PROJECT_BINARY_DIR}/bin/fake2eelocksucceeded.json" COPYONLY) diff --git a/test/testclientstatusreporting.cpp b/test/testclientstatusreporting.cpp new file mode 100644 index 0000000000000..e9ff338bd77ef --- /dev/null +++ b/test/testclientstatusreporting.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * 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 "account.h" +#include "accountstate.h" +#include "clientstatusreporting.h" +#include "syncenginetestutils.h" + +#include +#include + +namespace { +static QByteArray fake200Response = R"({"ocs":{"meta":{"status":"success","statuscode":200},"data":[]}})"; +} + +class TestClientStatusReporting : public QObject +{ + Q_OBJECT + +public: + TestClientStatusReporting() = default; + + QScopedPointer fakeQnam; + OCC::Account *account; + QScopedPointer accountState; + QString dbFilePath; + QVariantMap bodyReceivedAndParsed; + +private slots: + void initTestCase() + { + OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000; + OCC::ClientStatusReporting::repordSendIntervalMs = 2000; + + fakeQnam.reset(new FakeQNAM({})); + account = OCC::Account::create().get(); + account->setCredentials(new FakeCredentials{fakeQnam.data()}); + account->setUrl(QUrl(("http://example.de"))); + + accountState.reset(new OCC::AccountState(account->sharedFromThis())); + + const auto databaseId = QStringLiteral("%1@%2").arg(account->davUser(), account->url().toString()); + const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); + dbFilePath = QDir::tempPath() + QStringLiteral("/.tests_userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); + QFile(dbFilePath).remove(); + OCC::ClientStatusReporting::dbPathForTesting = dbFilePath; + + QVariantMap capabilities; + capabilities[QStringLiteral("security_guard")] = QVariantMap{ + { QStringLiteral("diagnostics"), true } + }; + account->setCapabilities(capabilities); + + fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) { + QNetworkReply *reply = nullptr; + const auto reqBody = device->readAll(); + bodyReceivedAndParsed = QJsonDocument::fromJson(reqBody).toVariant().toMap(); + reply = new FakePayloadReply(op, req, fake200Response, fakeQnam.data()); + return reply; + }); + } + + void testReportAndSendStatuses() + { + for (int i = 0; i < 2; ++i) { + // 5 conflicts + account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Conflict); + account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); + account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_Conflict); + account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); + account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ConflictCaseClash); + + // 4 problems + account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_ServerError); + account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ServerError); + account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_Virtual_File_Hydration_Failure); + // 3 occurances of UploadError_No_Write_Permissions + account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); + account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); + account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); + QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs); + + QVERIFY(!bodyReceivedAndParsed.isEmpty()); + + // we must have "virus_detected" and "e2e_errors" keys present (as required by server) + QVERIFY(bodyReceivedAndParsed.contains("virus_detected")); + QVERIFY(bodyReceivedAndParsed.contains("e2e_errors")); + + // we must have 5 conflicts + const auto conflictsReceived = bodyReceivedAndParsed.value("sync_conflicts").toMap(); + QVERIFY(!conflictsReceived.isEmpty()); + QCOMPARE(conflictsReceived.value("count"), 5); + + // we must have 4 problems + const auto problemsReceived = bodyReceivedAndParsed.value("problems").toMap(); + QVERIFY(!problemsReceived.isEmpty()); + QCOMPARE(problemsReceived.size(), 4); + const auto problemsNoWritePermissions = problemsReceived.value(OCC::ClientStatusReporting::statusStringFromNumber(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions)).toMap(); + // among those, 3 occurances of UploadError_No_Write_Permissions + QCOMPARE(problemsNoWritePermissions.value("count"), 3); + + bodyReceivedAndParsed.clear(); + } + } + + void testNothingReportedAndNothingSent() + { + QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs); + QVERIFY(bodyReceivedAndParsed.isEmpty()); + } + + void cleanupTestCase() + { + accountState.reset(nullptr); + delete account; + QFile(dbFilePath).remove(); + } +}; + +QTEST_MAIN(TestClientStatusReporting) +#include "testclientstatusreporting.moc" From b13c9d9e2d3668f5f3755c744fda2c437f68feeb Mon Sep 17 00:00:00 2001 From: alex-z Date: Mon, 27 Nov 2023 22:12:26 +0100 Subject: [PATCH 6/9] Prevent issues when changing the enum. Signed-off-by: alex-z --- src/common/common.cmake | 1 - src/gui/CMakeLists.txt | 4 +- src/{libsync => gui}/ocsjob.cpp | 0 src/{libsync => gui}/ocsjob.h | 8 +- src/libsync/CMakeLists.txt | 7 +- src/libsync/account.cpp | 17 +- src/libsync/clientstatusreporting.cpp | 153 +++++++++++------- src/libsync/clientstatusreporting.h | 80 +++++---- .../clientstatusreportingrecord.cpp | 2 +- .../clientstatusreportingrecord.h | 6 +- src/libsync/networkjobs.cpp | 2 +- src/libsync/ocsclientstatusreportingjob.cpp | 46 ------ src/libsync/ocsclientstatusreportingjob.h | 42 ----- src/libsync/propagatedownload.cpp | 3 +- 14 files changed, 165 insertions(+), 206 deletions(-) rename src/{libsync => gui}/ocsjob.cpp (100%) rename src/{libsync => gui}/ocsjob.h (99%) rename src/{common => libsync}/clientstatusreportingrecord.cpp (89%) rename src/{common => libsync}/clientstatusreportingrecord.h (89%) delete mode 100644 src/libsync/ocsclientstatusreportingjob.cpp delete mode 100644 src/libsync/ocsclientstatusreportingjob.h diff --git a/src/common/common.cmake b/src/common/common.cmake index f25151ebbd95e..671973579f0bb 100644 --- a/src/common/common.cmake +++ b/src/common/common.cmake @@ -4,7 +4,6 @@ set(common_SOURCES ${CMAKE_CURRENT_LIST_DIR}/checksums.cpp ${CMAKE_CURRENT_LIST_DIR}/checksumcalculator.cpp - ${CMAKE_CURRENT_LIST_DIR}/clientstatusreportingrecord.cpp ${CMAKE_CURRENT_LIST_DIR}/filesystembase.cpp ${CMAKE_CURRENT_LIST_DIR}/ownsql.cpp ${CMAKE_CURRENT_LIST_DIR}/preparedsqlquerymanager.cpp diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 3befe15452465..1968683e23415 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -121,10 +121,10 @@ set(client_SRCS navigationpanehelper.cpp networksettings.h networksettings.cpp - "${CMAKE_SOURCE_DIR}/src/libsync/ocsjob.h" - "${CMAKE_SOURCE_DIR}/src/libsync/ocsjob.cpp" ocsnavigationappsjob.h ocsnavigationappsjob.cpp + ocsjob.h + ocsjob.cpp ocssharejob.h ocssharejob.cpp ocsshareejob.h diff --git a/src/libsync/ocsjob.cpp b/src/gui/ocsjob.cpp similarity index 100% rename from src/libsync/ocsjob.cpp rename to src/gui/ocsjob.cpp diff --git a/src/libsync/ocsjob.h b/src/gui/ocsjob.h similarity index 99% rename from src/libsync/ocsjob.h rename to src/gui/ocsjob.h index e22ac73abf5ff..47e13e037b016 100644 --- a/src/libsync/ocsjob.h +++ b/src/gui/ocsjob.h @@ -148,13 +148,11 @@ protected slots: private slots: bool finished() override; -protected: - QByteArray _verb; - QNetworkRequest _request; - private: - QVector _passStatusCodes; + QByteArray _verb; QHash _params; + QVector _passStatusCodes; + QNetworkRequest _request; }; } diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 2944246e13d94..bd7931a8670d3 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -24,7 +24,10 @@ set(libsync_SRCS capabilities.cpp clientproxy.h clientproxy.cpp + clientstatusreporting.h clientstatusreporting.cpp + clientstatusreportingrecord.h + clientstatusreportingrecord.cpp cookiejar.h cookiejar.cpp discovery.h @@ -54,10 +57,6 @@ set(libsync_SRCS owncloudpropagator.cpp nextcloudtheme.h nextcloudtheme.cpp - ocsjob.h - ocsjob.cpp - ocsclientstatusreportingjob.h - ocsclientstatusreportingjob.cpp abstractpropagateremotedeleteencrypted.h abstractpropagateremotedeleteencrypted.cpp deletejob.h diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 876efca66652f..1d27a74ffaca2 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -64,8 +64,7 @@ constexpr int checksumRecalculateRequestServerVersionMinSupportedMajor = 24; constexpr auto isSkipE2eeMetadataChecksumValidationAllowedInClientVersion = MIRALL_VERSION_MAJOR == 3 && MIRALL_VERSION_MINOR == 8; } -namespace OCC -{ +namespace OCC { Q_LOGGING_CATEGORY(lcAccount, "nextcloud.sync.account", QtInfoMsg) const char app_password[] = "_app-password"; @@ -88,7 +87,7 @@ AccountPtr Account::create() return acc; } -ClientSideEncryption *Account::e2e() +ClientSideEncryption* Account::e2e() { // Qt expects everything in the connect to be a pointer, so return a pointer. return &_e2e; @@ -268,10 +267,14 @@ void Account::setCredentials(AbstractCredentials *cred) if (proxy.type() != QNetworkProxy::DefaultProxy) { _am->setProxy(proxy); } - connect(_am.data(), &QNetworkAccessManager::sslErrors, this, &Account::slotHandleSslErrors); - connect(_am.data(), &QNetworkAccessManager::proxyAuthenticationRequired, this, &Account::proxyAuthenticationRequired); - connect(_credentials.data(), &AbstractCredentials::fetched, this, &Account::slotCredentialsFetched); - connect(_credentials.data(), &AbstractCredentials::asked, this, &Account::slotCredentialsAsked); + connect(_am.data(), &QNetworkAccessManager::sslErrors, + this, &Account::slotHandleSslErrors); + connect(_am.data(), &QNetworkAccessManager::proxyAuthenticationRequired, + this, &Account::proxyAuthenticationRequired); + connect(_credentials.data(), &AbstractCredentials::fetched, + this, &Account::slotCredentialsFetched); + connect(_credentials.data(), &AbstractCredentials::asked, + this, &Account::slotCredentialsAsked); trySetupPushNotifications(); } diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp index 3311141e869e5..0db5acb71f2f6 100644 --- a/src/libsync/clientstatusreporting.cpp +++ b/src/libsync/clientstatusreporting.cpp @@ -12,16 +12,17 @@ * for more details. */ #include "clientstatusreporting.h" -#include "creds/abstractcredentials.h" + #include "account.h" -#include "common/clientstatusreportingrecord.h" -#include "common/syncjournaldb.h" +#include "clientstatusreportingrecord.h" #include +#include "common/c_jhash.h" #include namespace { constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; +constexpr auto statusNamesHash = "statusNamesHash"; } namespace OCC @@ -29,8 +30,8 @@ namespace OCC Q_LOGGING_CATEGORY(lcClientStatusReporting, "nextcloud.sync.clientstatusreporting", QtInfoMsg) ClientStatusReporting::ClientStatusReporting(Account *account, QObject *parent) - : _account(account) - , QObject(parent) + : QObject(parent) + , _account(account) { init(); } @@ -44,18 +45,18 @@ ClientStatusReporting::~ClientStatusReporting() void ClientStatusReporting::init() { + Q_ASSERT(!_isInitialized); if (_isInitialized) { qCDebug(lcClientStatusReporting) << "Double call to init"; return; } - for (int i = 0; i < ClientStatusReporting::Count; ++i) { + for (int i = 0; i < ClientStatusReporting::Status::Count; ++i) { const auto statusString = statusStringFromNumber(static_cast(i)); - _statusNamesAndHashes[i] = {statusString, SyncJournalDb::getPHash(statusString)}; + _statusNamesAndHashes[i] = {statusString, c_jhash64((uint8_t *)statusString.data(), statusString.size(), 0)}; } const auto dbPath = makeDbPath(); - _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); _database.setDatabaseName(dbPath); @@ -65,23 +66,42 @@ void ClientStatusReporting::init() } QSqlQuery query; - const auto prepareResult = query.prepare( + const auto prepareResult = query.prepare(QStringLiteral( "CREATE TABLE IF NOT EXISTS clientstatusreporting(" - "nHash INTEGER(8) PRIMARY KEY," + "name VARCHAR(4096) PRIMARY KEY," "status INTEGER(8)," - "name VARCHAR(4096)," "count INTEGER," - "lastOccurrence INTEGER(8))"); + "lastOccurrence INTEGER(8))")); if (!prepareResult || !query.exec()) { qCDebug(lcClientStatusReporting) << "Could not setup client clientstatusreporting table:" << query.lastError().text(); return; } - if (!query.prepare("CREATE TABLE IF NOT EXISTS keyvalue(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key))") || !query.exec()) { + if (!query.prepare(QStringLiteral("CREATE INDEX IF NOT EXISTS name ON clientstatusreporting(name);")) || !query.exec()) { + qCDebug(lcClientStatusReporting) << "Could not create index on clientstatusreporting table:" << query.lastError().text(); + return; + } + + if (!query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS keyvalue(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key))")) || !query.exec()) { qCDebug(lcClientStatusReporting) << "Could not setup client keyvalue table:" << query.lastError().text(); return; } + // prevent issues in case enum gets changed in future, hash its value and clean the db in case there was a change + QByteArray statusNamesContatenated; + for (int i = 0; i < ClientStatusReporting::Status::Count; ++i) { + statusNamesContatenated += statusStringFromNumber(static_cast(i)); + } + statusNamesContatenated += QByteArray::number(ClientStatusReporting::Status::Count); + const auto statusNamesHashCurrent = QCryptographicHash::hash(statusNamesContatenated, QCryptographicHash::Md5).toHex(); + const auto statusNamesHashFromDb = getStatusNamesHash(); + + if (statusNamesHashCurrent != statusNamesHashFromDb) { + deleteClientStatusReportingRecords(); + setStatusNamesHash(statusNamesHashCurrent); + } + // + _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval); connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReporting::sendReportToServer); _clientStatusReportingSendTimer.start(); @@ -96,40 +116,31 @@ QVector ClientStatusReporting::getClientStatusRepor QMutexLocker locker(&_mutex); QSqlQuery query; - const auto prepareResult = query.prepare("SELECT * FROM clientstatusreporting"); - - if (!prepareResult || !query.exec()) { - const auto errorMessage = query.lastError().text(); - qCDebug(lcClientStatusReporting) << "Could not get records from clientstatusreporting:" << errorMessage; + if (!query.prepare(QStringLiteral("SELECT * FROM clientstatusreporting")) || !query.exec()) { + qCDebug(lcClientStatusReporting) << "Could not get records from clientstatusreporting:" << query.lastError().text(); return records; } while (query.next()) { ClientStatusReportingRecord record; - record._nameHash = query.value(query.record().indexOf("nHash")).toLongLong(); - record._status = query.value(query.record().indexOf("status")).toLongLong(); - record._name = query.value(query.record().indexOf("name")).toByteArray(); - record._numOccurences = query.value(query.record().indexOf("count")).toLongLong(); - record._lastOccurence = query.value(query.record().indexOf("lastOccurrence")).toLongLong(); + record._status = query.value(query.record().indexOf(QStringLiteral("status"))).toLongLong(); + record._name = query.value(query.record().indexOf(QStringLiteral("name"))).toByteArray(); + record._numOccurences = query.value(query.record().indexOf(QStringLiteral("count"))).toLongLong(); + record._lastOccurence = query.value(query.record().indexOf(QStringLiteral("lastOccurrence"))).toLongLong(); records.push_back(record); } return records; } -bool ClientStatusReporting::deleteClientStatusReportingRecords() +void ClientStatusReporting::deleteClientStatusReportingRecords() const { QSqlQuery query; - const auto prepareResult = query.prepare("DELETE FROM clientstatusreporting"); - - if (!prepareResult || !query.exec()) { - const auto errorMessage = query.lastError().text(); - qCDebug(lcClientStatusReporting) << "Could not get records from clientstatusreporting:" << errorMessage; - return false; + if (!query.prepare(QStringLiteral("DELETE FROM clientstatusreporting")) || !query.exec()) { + qCDebug(lcClientStatusReporting) << "Could not delete records from clientstatusreporting:" << query.lastError().text(); } - return true; } -Result ClientStatusReporting::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) +Result ClientStatusReporting::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const { Q_ASSERT(record.isValid()); if (!record.isValid()) { @@ -144,13 +155,12 @@ Result ClientStatusReporting::setClientStatusReportingRecord(cons QSqlQuery query; const auto prepareResult = query.prepare( - "INSERT OR REPLACE INTO clientstatusreporting (nHash, name, status, count, lastOccurrence) VALUES(:nHash, :name, :status, :count, :lastOccurrence) ON CONFLICT(nHash) " - "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;"); - query.bindValue(":nHash", recordCopy._nameHash); - query.bindValue(":name", recordCopy._name); - query.bindValue(":status", recordCopy._status); - query.bindValue(":count", 1); - query.bindValue(":lastOccurrence", recordCopy._lastOccurence); + QStringLiteral("INSERT OR REPLACE INTO clientstatusreporting (name, status, count, lastOccurrence) VALUES(:name, :status, :count, :lastOccurrence) ON CONFLICT(name) " + "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;")); + query.bindValue(QStringLiteral(":name"), recordCopy._name); + query.bindValue(QStringLiteral(":status"), recordCopy._status); + query.bindValue(QStringLiteral(":count"), 1); + query.bindValue(QStringLiteral(":lastOccurrence"), recordCopy._lastOccurence); if (!prepareResult || !query.exec()) { const auto errorMessage = query.lastError().text(); @@ -161,10 +171,10 @@ Result ClientStatusReporting::setClientStatusReportingRecord(cons return {}; } -void ClientStatusReporting::reportClientStatus(const Status status) +void ClientStatusReporting::reportClientStatus(const Status status) const { if (!_isInitialized) { - qCWarning(lcClientStatusReporting) << "Could not report status. Status reporting is not initialized"; + qCDebug(lcClientStatusReporting) << "Could not report status. Status reporting is not initialized"; return; } Q_ASSERT(status >= 0 && status < Count); @@ -176,7 +186,6 @@ void ClientStatusReporting::reportClientStatus(const Status status) ClientStatusReportingRecord record; record._name = _statusNamesAndHashes[status].first; record._status = status; - record._nameHash = _statusNamesAndHashes[status].second; record._lastOccurence = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); const auto result = setClientStatusReportingRecord(record); if (!result.isValid()) { @@ -207,8 +216,8 @@ void ClientStatusReporting::sendReportToServer() clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json, int statusCode) { if (statusCode == 0 || statusCode == 200 || statusCode == 201 || statusCode == 204) { - const auto metaFromJson = json.object().value("ocs").toObject().value("meta").toObject(); - const auto codeFromJson = metaFromJson.value("statuscode").toInt(); + const auto metaFromJson = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("meta")).toObject(); + const auto codeFromJson = metaFromJson.value(QStringLiteral("statuscode")).toInt(); if (codeFromJson == 0 || codeFromJson == 200 || codeFromJson == 201 || codeFromJson == 204) { reportToServerSentSuccessfully(); return; @@ -221,9 +230,7 @@ void ClientStatusReporting::sendReportToServer() void ClientStatusReporting::reportToServerSentSuccessfully() { - if (!deleteClientStatusReportingRecords()) { - qCDebug(lcClientStatusReporting) << "Error deleting client status report."; - } + deleteClientStatusReportingRecords(); setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()); } @@ -238,12 +245,12 @@ QString ClientStatusReporting::makeDbPath() const return ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); } -qulonglong ClientStatusReporting::getLastSentReportTimestamp() const +quint64 ClientStatusReporting::getLastSentReportTimestamp() const { QMutexLocker locker(&_mutex); QSqlQuery query; - const auto prepareResult = query.prepare("SELECT value FROM keyvalue WHERE key = (:key)"); - query.bindValue(":key", lastSentReportTimestamp); + const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); + query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); if (!prepareResult || !query.exec()) { qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; return 0; @@ -252,9 +259,37 @@ qulonglong ClientStatusReporting::getLastSentReportTimestamp() const qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table:" << query.lastError().text(); return 0; } + return query.value(query.record().indexOf(QStringLiteral("value"))).toULongLong(); +} - int valueIndex = query.record().indexOf("value"); - return query.value(valueIndex).toULongLong(); +void ClientStatusReporting::setStatusNamesHash(const QByteArray &hash) const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); + query.bindValue(QStringLiteral(":key"), statusNamesHash); + query.bindValue(QStringLiteral(":value"), hash); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReporting) << "Could not set status names hash."; + return; + } +} + +QByteArray ClientStatusReporting::getStatusNamesHash() const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); + query.bindValue(QStringLiteral(":key"), statusNamesHash); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReporting) << "Could not get status names hash. No such record:" << statusNamesHash; + return {}; + } + if (!query.next()) { + qCDebug(lcClientStatusReporting) << "Could not get status names hash:" << query.lastError().text(); + return {}; + } + return query.value(query.record().indexOf(QStringLiteral("value"))).toByteArray(); } QVariantMap ClientStatusReporting::prepareReport() const @@ -294,13 +329,13 @@ QVariantMap ClientStatusReporting::prepareReport() const return report; } -void ClientStatusReporting::setLastSentReportTimestamp(const qulonglong timestamp) +void ClientStatusReporting::setLastSentReportTimestamp(const quint64 timestamp) const { QMutexLocker locker(&_mutex); QSqlQuery query; - const auto prepareResult = query.prepare("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);"); - query.bindValue(":key", lastSentReportTimestamp); - query.bindValue(":value", timestamp); + const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); + query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); + query.bindValue(QStringLiteral(":value"), timestamp); if (!prepareResult || !query.exec()) { qCDebug(lcClientStatusReporting) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; return; @@ -346,7 +381,7 @@ QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) return {}; } -QString ClientStatusReporting::classifyStatus(const Status status) +QByteArray ClientStatusReporting::classifyStatus(const Status status) { Q_ASSERT(status >= 0 && status < Count); if (status < 0 || status >= Status::Count) { @@ -360,7 +395,7 @@ QString ClientStatusReporting::classifyStatus(const Status status) case DownloadError_ConflictInvalidCharacters: case UploadError_Conflict: case UploadError_ConflictInvalidCharacters: - return QStringLiteral("sync_conflicts"); + return QByteArrayLiteral("sync_conflicts"); case DownloadError_Cannot_Create_File: case DownloadError_No_Free_Space: case DownloadError_ServerError: @@ -375,6 +410,6 @@ QString ClientStatusReporting::classifyStatus(const Status status) return {}; } int ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000 * 60 * 2; // check if the time has come, every 2 minutes -int ClientStatusReporting::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours +quint64 ClientStatusReporting::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours QString ClientStatusReporting::dbPathForTesting; } diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h index 70e232086a72b..cbe696bbe7072 100644 --- a/src/libsync/clientstatusreporting.h +++ b/src/libsync/clientstatusreporting.h @@ -14,19 +14,17 @@ #pragma once #include "owncloudlib.h" -#include "accountfwd.h" #include -#include -#include +#include #include -#include -#include -#include +#include +#include #include #include - -class TestClientStatusReporting; +#include +#include +#include namespace OCC { @@ -38,57 +36,73 @@ class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject Q_OBJECT public: enum Status { - DownloadError_Cannot_Create_File = 100, - DownloadError_Conflict = 101, - DownloadError_ConflictCaseClash = 102, - DownloadError_ConflictInvalidCharacters = 103, - DownloadError_No_Free_Space = 104, - DownloadError_ServerError = 105, - DownloadError_Virtual_File_Hydration_Failure = 106, - UploadError_Conflict = 107, - UploadError_ConflictInvalidCharacters = 108, - UploadError_No_Free_Space = 109, - UploadError_No_Write_Permissions = 110, - UploadError_ServerError = 111, - Count = UploadError_ServerError + 1, + DownloadError_Cannot_Create_File = 0, + DownloadError_Conflict, + DownloadError_ConflictCaseClash, + DownloadError_ConflictInvalidCharacters, + DownloadError_No_Free_Space, + DownloadError_ServerError, + DownloadError_Virtual_File_Hydration_Failure, + UploadError_Conflict, + UploadError_ConflictInvalidCharacters, + UploadError_No_Free_Space, + UploadError_No_Write_Permissions, + UploadError_ServerError, + Count, }; explicit ClientStatusReporting(Account *account, QObject *parent = nullptr); - ~ClientStatusReporting(); + ~ClientStatusReporting() override; + + static QByteArray statusStringFromNumber(const Status status); private: void init(); - void reportClientStatus(const Status status); + // reporting must happen via Account + void reportClientStatus(const Status status) const; - [[nodiscard]] Result setClientStatusReportingRecord(const ClientStatusReportingRecord &record); + [[nodiscard]] Result setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const; [[nodiscard]] QVector getClientStatusReportingRecords() const; - [[nodiscard]] bool deleteClientStatusReportingRecords(); - void setLastSentReportTimestamp(const qulonglong timestamp); - [[nodiscard]] qulonglong getLastSentReportTimestamp() const; + void deleteClientStatusReportingRecords() const; + + void setLastSentReportTimestamp(const quint64 timestamp) const; + [[nodiscard]] quint64 getLastSentReportTimestamp() const; + + void setStatusNamesHash(const QByteArray &hash) const; + [[nodiscard]] QByteArray getStatusNamesHash() const; + [[nodiscard]] QVariantMap prepareReport() const; void reportToServerSentSuccessfully(); + [[nodiscard]] QString makeDbPath() const; private slots: void sendReportToServer(); private: - static QByteArray statusStringFromNumber(const Status status); - static QString classifyStatus(const Status status); + static QByteArray classifyStatus(const Status status); +public: static int clientStatusReportingTrySendTimerInterval; - static int repordSendIntervalMs; - + static quint64 repordSendIntervalMs; + // this must be set in unit tests on init static QString dbPathForTesting; +private: + Account *_account = nullptr; - QHash> _statusNamesAndHashes; + QSqlDatabase _database; + bool _isInitialized = false; + QTimer _clientStatusReportingSendTimer; + + QHash> _statusNamesAndHashes; + + // inspired by SyncJournalDb mutable QRecursiveMutex _mutex; friend class Account; - friend class TestClientStatusReporting; }; } diff --git a/src/common/clientstatusreportingrecord.cpp b/src/libsync/clientstatusreportingrecord.cpp similarity index 89% rename from src/common/clientstatusreportingrecord.cpp rename to src/libsync/clientstatusreportingrecord.cpp index 95b0cda632d9f..4d795de64b3eb 100644 --- a/src/common/clientstatusreportingrecord.cpp +++ b/src/libsync/clientstatusreportingrecord.cpp @@ -19,6 +19,6 @@ namespace OCC bool ClientStatusReportingRecord::isValid() const { - return _status >= 0 && !_name.isEmpty() && _nameHash > 0 && _lastOccurence > 0; + return _status >= 0 && !_name.isEmpty() && _lastOccurence > 0; } } diff --git a/src/common/clientstatusreportingrecord.h b/src/libsync/clientstatusreportingrecord.h similarity index 89% rename from src/common/clientstatusreportingrecord.h rename to src/libsync/clientstatusreportingrecord.h index ba0b05162938c..6595177ec0995 100644 --- a/src/common/clientstatusreportingrecord.h +++ b/src/libsync/clientstatusreportingrecord.h @@ -12,7 +12,7 @@ * for more details. */ #pragma once -#include "ocsynclib.h" +#include "owncloudlib.h" #include #include @@ -23,10 +23,10 @@ namespace OCC * @brief The ClientStatusReportingRecord class * @ingroup libsync */ -struct OCSYNC_EXPORT ClientStatusReportingRecord { + +struct OWNCLOUDSYNC_EXPORT ClientStatusReportingRecord { QByteArray _name; int _status = -1; - quint64 _nameHash = 0; quint64 _numOccurences = 1; quint64 _lastOccurence = 0; diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp index 5b2722848ab42..3c784675feb61 100644 --- a/src/libsync/networkjobs.cpp +++ b/src/libsync/networkjobs.cpp @@ -943,7 +943,7 @@ JsonApiJob::JsonApiJob(const AccountPtr &account, const QString &path, QObject * void JsonApiJob::setBody(const QJsonDocument &body) { - SimpleApiJob::setBody(body.toJson(QJsonDocument::JsonFormat::Compact)); + SimpleApiJob::setBody(body.toJson()); qCDebug(lcJsonApiJob) << "Set body for request:" << SimpleApiJob::body(); if (!SimpleApiJob::body().isEmpty()) { request().setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); diff --git a/src/libsync/ocsclientstatusreportingjob.cpp b/src/libsync/ocsclientstatusreportingjob.cpp deleted file mode 100644 index ff9e0baf0668f..0000000000000 --- a/src/libsync/ocsclientstatusreportingjob.cpp +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2023 by Oleksandr Zolotov - * - * 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 "ocsclientstatusreportingjob.h" -#include "networkjobs.h" -#include "account.h" - -#include -#include - -namespace OCC { - -OcsClientStatusReportingJob::OcsClientStatusReportingJob(AccountPtr account) - : OcsJob(account) -{ - setPath(QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); - connect(this, &OcsJob::jobFinished, this, &OcsClientStatusReportingJob::jobDone); -} - -void OcsClientStatusReportingJob::sendStatusReport(const QVariant &jsonData) -{ - setVerb("PUT"); - - addRawHeader("Ocs-APIREQUEST", "true"); - addRawHeader("Content-Type", "application/json"); - - const auto url = Utility::concatUrlPath(account()->url(), path()); - sendRequest(_verb, url, _request, QJsonDocument::fromVariant(jsonData.toMap()).toJson()); - AbstractNetworkJob::start(); -} - -void OcsClientStatusReportingJob::jobDone(QJsonDocument reply) -{ - emit jobFinished(reply, {}); -} -} diff --git a/src/libsync/ocsclientstatusreportingjob.h b/src/libsync/ocsclientstatusreportingjob.h deleted file mode 100644 index c42cdc2bbe16c..0000000000000 --- a/src/libsync/ocsclientstatusreportingjob.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2023 by Oleksandr Zolotov - * - * 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 "ocsjob.h" - -#include -#include - -namespace OCC { - -/** - * @brief The OcsClientStatusReportingJob class - * @ingroup gui - * - * Handle sending client status reports via OCS Diagnostics API. - */ -class OcsClientStatusReportingJob : public OcsJob -{ - Q_OBJECT -public: - explicit OcsClientStatusReportingJob(AccountPtr account); - void sendStatusReport(const QVariant &jsonData); - -signals: - void jobFinished(QJsonDocument reply, QVariant value); - -private slots: - void jobDone(QJsonDocument reply); -}; -} diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index 456e079a568b6..b78adaae1c46c 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -1282,9 +1282,8 @@ void PropagateDownloadFile::downloadFinished() // Maybe what we downloaded was a conflict file? If so, set a conflict record. // (the data was prepared in slotGetFinished above) - if (_conflictRecord.isValid()) { + if (_conflictRecord.isValid()) propagator()->_journal->setConflictRecord(_conflictRecord); - } if (vfs && vfs->mode() == Vfs::WithSuffix) { // If the virtual file used to have a different name and db From 5f299421ed887761a9c167dc68a629bbb8a03618 Mon Sep 17 00:00:00 2001 From: alex-z Date: Thu, 30 Nov 2023 14:25:33 +0100 Subject: [PATCH 7/9] Allow sending E2EE and Virus_Detected statuses. Also updated tests. Signed-off-by: alex-z --- src/gui/tray/NCBusyIndicator.qml | 2 +- src/libsync/clientstatusreporting.cpp | 73 ++++++++++++++++++--------- src/libsync/clientstatusreporting.h | 2 + test/testclientstatusreporting.cpp | 21 ++++++-- 4 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/gui/tray/NCBusyIndicator.qml b/src/gui/tray/NCBusyIndicator.qml index 973d3dee1ed62..ddead28d4b07d 100644 --- a/src/gui/tray/NCBusyIndicator.qml +++ b/src/gui/tray/NCBusyIndicator.qml @@ -42,7 +42,7 @@ BusyIndicator { RotationAnimator { target: contentImage - running: root.running + running: false onRunningChanged: contentImage.rotation = 0 from: 0 to: 360 diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp index 0db5acb71f2f6..617405047c5ec 100644 --- a/src/libsync/clientstatusreporting.cpp +++ b/src/libsync/clientstatusreporting.cpp @@ -23,6 +23,11 @@ namespace { constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; constexpr auto statusNamesHash = "statusNamesHash"; + +constexpr auto statusReportCategoryE2eErrors = "e2e_errors"; +constexpr auto statusReportCategoryProblems = "problems"; +constexpr auto statusReportCategorySyncConflicts = "sync_conflicts"; +constexpr auto statusReportCategoryVirus = "virus_detected"; } namespace OCC @@ -300,13 +305,15 @@ QVariantMap ClientStatusReporting::prepareReport() const } QVariantMap report; - report[QStringLiteral("sync_conflicts")] = QVariantMap{}; - report[QStringLiteral("problems")] = QVariantMap{}; - report[QStringLiteral("virus_detected")] = QVariantMap{}; - report[QStringLiteral("e2e_errors")] = QVariantMap{}; + report[statusReportCategorySyncConflicts] = QVariantMap{}; + report[statusReportCategoryProblems] = QVariantMap{}; + report[statusReportCategoryVirus] = QVariantMap{}; + report[statusReportCategoryE2eErrors] = QVariantMap{}; - QVariantMap syncConflicts; + QVariantMap e2eeErrors; QVariantMap problems; + QVariantMap syncConflicts; + QVariantMap virusDetectedErrors; for (const auto &record : records) { const auto categoryKey = classifyStatus(static_cast(record._status)); @@ -315,15 +322,25 @@ QVariantMap ClientStatusReporting::prepareReport() const qCDebug(lcClientStatusReporting) << "Could not classify status:"; continue; } - - if (categoryKey == QStringLiteral("sync_conflicts")) { + + if (categoryKey == statusReportCategoryE2eErrors) { + const auto initialCount = e2eeErrors[QStringLiteral("count")].toInt(); + e2eeErrors[QStringLiteral("count")] = initialCount + record._numOccurences; + e2eeErrors[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = e2eeErrors; + } else if (categoryKey == statusReportCategoryProblems) { + problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}}; + report[categoryKey] = problems; + } else if (categoryKey == statusReportCategorySyncConflicts) { const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; report[categoryKey] = syncConflicts; - } else if (categoryKey == QStringLiteral("problems")) { - problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}}; - report[categoryKey] = problems; + } else if (categoryKey == statusReportCategoryVirus) { + const auto initialCount = virusDetectedErrors[QStringLiteral("count")].toInt(); + virusDetectedErrors[QStringLiteral("count")] = initialCount + record._numOccurences; + virusDetectedErrors[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = virusDetectedErrors; } } return report; @@ -352,29 +369,33 @@ QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) switch (status) { case DownloadError_Cannot_Create_File: - return QByteArrayLiteral("DownloadError.CANNOT_CREATE_FILE"); + return QByteArrayLiteral("DownloadResult.CANNOT_CREATE_FILE"); case DownloadError_Conflict: - return QByteArrayLiteral("DownloadError.CONFLICT"); + return QByteArrayLiteral("DownloadResult.CONFLICT"); case DownloadError_ConflictCaseClash: - return QByteArrayLiteral("DownloadError.CONFLICT_CASECLASH"); + return QByteArrayLiteral("DownloadResult.CONFLICT_CASECLASH"); case DownloadError_ConflictInvalidCharacters: - return QByteArrayLiteral("DownloadError.CONFLICT_INVALID_CHARACTERS"); + return QByteArrayLiteral("DownloadResult.CONFLICT_INVALID_CHARACTERS"); case DownloadError_No_Free_Space: - return QByteArrayLiteral("DownloadError.NO_FREE_SPACE"); + return QByteArrayLiteral("DownloadResult.NO_FREE_SPACE"); case DownloadError_ServerError: - return QByteArrayLiteral("DownloadError.SERVER_ERROR"); + return QByteArrayLiteral("DownloadResult.SERVER_ERROR"); case DownloadError_Virtual_File_Hydration_Failure: - return QByteArrayLiteral("DownloadError.VIRTUAL_FILE_HYDRATION_FAILURE "); + return QByteArrayLiteral("DownloadResult.VIRTUAL_FILE_HYDRATION_FAILURE"); + case E2EeError_GeneralError: + return QByteArrayLiteral("E2EeError.General"); case UploadError_Conflict: - return QByteArrayLiteral("UploadError.CONFLICT_CASECLASH"); + return QByteArrayLiteral("UploadResult.CONFLICT_CASECLASH"); case UploadError_ConflictInvalidCharacters: - return QByteArrayLiteral("UploadError.CONFLICT_INVALID_CHARACTERS"); + return QByteArrayLiteral("UploadResult.CONFLICT_INVALID_CHARACTERS"); case UploadError_No_Free_Space: - return QByteArrayLiteral("UploadError.NO_FREE_SPACE"); + return QByteArrayLiteral("UploadResult.NO_FREE_SPACE"); case UploadError_No_Write_Permissions: - return QByteArrayLiteral("UploadError.NO_WRITE_PERMISSIONS"); + return QByteArrayLiteral("UploadResult.NO_WRITE_PERMISSIONS"); case UploadError_ServerError: - return QByteArrayLiteral("UploadError.SERVER_ERROR"); + return QByteArrayLiteral("UploadResult.SERVER_ERROR"); + case UploadError_Virus_Detected: + return QByteArrayLiteral("UploadResult.VIRUS_DETECTED"); case Count: return {}; }; @@ -395,7 +416,7 @@ QByteArray ClientStatusReporting::classifyStatus(const Status status) case DownloadError_ConflictInvalidCharacters: case UploadError_Conflict: case UploadError_ConflictInvalidCharacters: - return QByteArrayLiteral("sync_conflicts"); + return statusReportCategorySyncConflicts; case DownloadError_Cannot_Create_File: case DownloadError_No_Free_Space: case DownloadError_ServerError: @@ -403,7 +424,11 @@ QByteArray ClientStatusReporting::classifyStatus(const Status status) case UploadError_No_Free_Space: case UploadError_No_Write_Permissions: case UploadError_ServerError: - return QByteArrayLiteral("problems"); + return statusReportCategoryProblems; + case UploadError_Virus_Detected: + return statusReportCategoryVirus; + case E2EeError_GeneralError: + return statusReportCategoryE2eErrors; case Count: return {}; }; diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h index cbe696bbe7072..70b7508c028b0 100644 --- a/src/libsync/clientstatusreporting.h +++ b/src/libsync/clientstatusreporting.h @@ -43,11 +43,13 @@ class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject DownloadError_No_Free_Space, DownloadError_ServerError, DownloadError_Virtual_File_Hydration_Failure, + E2EeError_GeneralError, UploadError_Conflict, UploadError_ConflictInvalidCharacters, UploadError_No_Free_Space, UploadError_No_Write_Permissions, UploadError_ServerError, + UploadError_Virus_Detected, Count, }; diff --git a/test/testclientstatusreporting.cpp b/test/testclientstatusreporting.cpp index e9ff338bd77ef..5686baaa7391c 100644 --- a/test/testclientstatusreporting.cpp +++ b/test/testclientstatusreporting.cpp @@ -88,13 +88,28 @@ private slots: account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); + + // 3 occurances of UploadError_Virus_Detected + account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected); + account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected); + account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected); + + // 2 occurances of E2EeError_GeneralError + account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs); QVERIFY(!bodyReceivedAndParsed.isEmpty()); - // we must have "virus_detected" and "e2e_errors" keys present (as required by server) - QVERIFY(bodyReceivedAndParsed.contains("virus_detected")); - QVERIFY(bodyReceivedAndParsed.contains("e2e_errors")); + // we must have 2 e2ee errors + const auto virusDetectedErrorsReceived = bodyReceivedAndParsed.value("virus_detected").toMap(); + QVERIFY(!virusDetectedErrorsReceived.isEmpty()); + QVERIFY(virusDetectedErrorsReceived.size(), 3); + + // we must have 2 e2ee errors + const auto e2eeErrorsReceived = bodyReceivedAndParsed.value("e2e_errors").toMap(); + QVERIFY(!e2eeErrorsReceived.isEmpty()); + QVERIFY(e2eeErrorsReceived.size(), 2); // we must have 5 conflicts const auto conflictsReceived = bodyReceivedAndParsed.value("sync_conflicts").toMap(); From ab95ae859af5ea7c1e25b2cf90c5739da143d29e Mon Sep 17 00:00:00 2001 From: alex-z Date: Thu, 30 Nov 2023 15:00:18 +0100 Subject: [PATCH 8/9] Report error Virus Detected. Updated tests. Signed-off-by: alex-z --- src/libsync/bulkpropagatorjob.cpp | 3 ++ src/libsync/owncloudpropagator.cpp | 10 ++++++- src/libsync/owncloudpropagator_p.h | 47 ++++++++++++++++++++++++++++++ src/libsync/propagateupload.cpp | 3 ++ src/libsync/propagateuploadng.cpp | 6 ++++ src/libsync/propagateuploadv1.cpp | 3 ++ src/libsync/syncfileitem.h | 2 ++ test/testnextcloudpropagator.cpp | 23 +++++++++++++++ 8 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/libsync/bulkpropagatorjob.cpp b/src/libsync/bulkpropagatorjob.cpp index 0f0a7db3fcfa4..58e1ab82f0c95 100644 --- a/src/libsync/bulkpropagatorjob.cpp +++ b/src/libsync/bulkpropagatorjob.cpp @@ -366,6 +366,9 @@ void BulkPropagatorJob::slotPutFinishedOneFile(const BulkUploadItem &singleFile, singleFile._item->_requestId = job->requestId(); if (singleFile._item->_httpErrorCode != 200) { commonErrorHandling(singleFile._item, fileReply[QStringLiteral("message")].toString()); + const auto exceptionParsed = getExceptionFromReply(job->reply()); + singleFile._item->_errorExceptionName = exceptionParsed.first; + singleFile._item->_errorExceptionMessage = exceptionParsed.second; return; } diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index b71f7261eac06..39b42f7518589 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -358,7 +358,15 @@ void PropagateItemJob::reportClientStatuses() propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); } else if (_item->_httpErrorCode != 0 && _item->_httpErrorCode != 200 && _item->_httpErrorCode != 201 && _item->_httpErrorCode != 204) { if (_item->_direction == SyncFileItem::Up) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError); + const auto isCodeBadReqOrUnsupportedMediaType = (_item->_httpErrorCode == 400 || _item->_httpErrorCode == 415); + const auto isExceptionInfoPresent = !_item->_errorExceptionName.isEmpty() && !_item->_errorExceptionMessage.isEmpty(); + if (isCodeBadReqOrUnsupportedMediaType && isExceptionInfoPresent + && _item->_errorExceptionName.contains(QStringLiteral("UnsupportedMediaType")) + && _item->_errorExceptionMessage.contains(QStringLiteral("virus"), Qt::CaseInsensitive)) { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Virus_Detected); + } else { + propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError); + } } else { propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ServerError); } diff --git a/src/libsync/owncloudpropagator_p.h b/src/libsync/owncloudpropagator_p.h index 6337b2f3cfb9c..7d0c121bb4af6 100644 --- a/src/libsync/owncloudpropagator_p.h +++ b/src/libsync/owncloudpropagator_p.h @@ -48,6 +48,7 @@ inline bool fileIsStillChanging(const OCC::SyncFileItem &item) namespace OCC { + inline QByteArray getEtagFromReply(QNetworkReply *reply) { QByteArray ocEtag = parseEtag(reply->rawHeader("OC-ETag")); @@ -62,6 +63,52 @@ inline QByteArray getEtagFromReply(QNetworkReply *reply) return ret; } +inline QPair getExceptionFromReply(QNetworkReply *reply) +{ + Q_ASSERT(reply); + if (!reply) { + qDebug() << "Could not parse null reply"; + return {}; + } + const auto httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + // only for BadRequest and UnsupportedMediaType + if (httpStatusCode != 400 && httpStatusCode != 415) { + return {}; + } + + // we must not modify the buffer + const auto replyBody = reply->peek(reply->bytesAvailable()); + + // parse exception name + auto exceptionNameStartIndex = replyBody.indexOf(QByteArrayLiteral("")); + if (exceptionNameStartIndex == -1) { + qDebug() << "Could not parse exception. No tag."; + return {}; + } + exceptionNameStartIndex += QByteArrayLiteral("").size(); + const auto exceptionNameEndIndex = replyBody.indexOf('<', exceptionNameStartIndex); + if (exceptionNameEndIndex == -1) { + return {}; + } + const auto exceptionName = replyBody.mid(exceptionNameStartIndex, exceptionNameEndIndex - exceptionNameStartIndex); + + // parse exception message + auto exceptionMessageStartIndex = replyBody.indexOf(QByteArrayLiteral(""), exceptionNameEndIndex); + if (exceptionMessageStartIndex == -1) { + qDebug() << "Could not parse exception. No tag."; + return {exceptionName, {}}; + } + exceptionMessageStartIndex += QByteArrayLiteral("").size(); + const auto exceptionMessageEndIndex = replyBody.indexOf('<', exceptionMessageStartIndex); + if (exceptionMessageEndIndex == -1) { + return {exceptionName, {}}; + } + + const auto exceptionMessage = replyBody.mid(exceptionMessageStartIndex, exceptionMessageEndIndex - exceptionMessageStartIndex); + + return {exceptionName, exceptionMessage}; +} + /** * Given an error from the network, map to a SyncFileItem::Status error */ diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp index f74727ec7bb80..78b04205cc361 100644 --- a/src/libsync/propagateupload.cpp +++ b/src/libsync/propagateupload.cpp @@ -112,6 +112,9 @@ bool PollJob::finished() _item->_requestId = requestId(); _item->_status = classifyError(err, _item->_httpErrorCode); _item->_errorString = errorString(); + const auto exceptionParsed = getExceptionFromReply(reply()); + _item->_errorExceptionName = exceptionParsed.first; + _item->_errorExceptionMessage = exceptionParsed.second; if (_item->_status == SyncFileItem::FatalError || _item->_httpErrorCode >= 400) { if (_item->_status != SyncFileItem::FatalError diff --git a/src/libsync/propagateuploadng.cpp b/src/libsync/propagateuploadng.cpp index 8be463ecd21c9..f751c58253fbe 100644 --- a/src/libsync/propagateuploadng.cpp +++ b/src/libsync/propagateuploadng.cpp @@ -411,6 +411,9 @@ void PropagateUploadFileNG::slotPutFinished() _item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); _item->_requestId = job->requestId(); commonErrorHandling(job); + const auto exceptionParsed = getExceptionFromReply(job->reply()); + _item->_errorExceptionName = exceptionParsed.first; + _item->_errorExceptionMessage = exceptionParsed.second; return; } @@ -496,6 +499,9 @@ void PropagateUploadFileNG::slotMoveJobFinished() if (err != QNetworkReply::NoError) { commonErrorHandling(job); + const auto exceptionParsed = getExceptionFromReply(job->reply()); + _item->_errorExceptionName = exceptionParsed.first; + _item->_errorExceptionMessage = exceptionParsed.second; return; } diff --git a/src/libsync/propagateuploadv1.cpp b/src/libsync/propagateuploadv1.cpp index 33cd8892d7335..a32e796a597de 100644 --- a/src/libsync/propagateuploadv1.cpp +++ b/src/libsync/propagateuploadv1.cpp @@ -218,6 +218,9 @@ void PropagateUploadFileV1::slotPutFinished() QNetworkReply::NetworkError err = job->reply()->error(); if (err != QNetworkReply::NoError) { commonErrorHandling(job); + const auto exceptionParsed = getExceptionFromReply(job->reply()); + _item->_errorExceptionName = exceptionParsed.first; + _item->_errorExceptionMessage = exceptionParsed.second; return; } diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 399546dcd0459..c0d880be6097f 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -287,6 +287,8 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem quint16 _httpErrorCode = 0; RemotePermissions _remotePerm; QString _errorString; // Contains a string only in case of error + QString _errorExceptionName; // Contains a server exception string only in case of error + QString _errorExceptionMessage; // Contains a server exception message string only in case of error QByteArray _responseTimeStamp; QByteArray _requestId; // X-Request-Id of the failed request quint32 _affectedItems = 1; // the number of affected items by the operation on this item. diff --git a/test/testnextcloudpropagator.cpp b/test/testnextcloudpropagator.cpp index 954c665f5f91c..812c5b43c7008 100644 --- a/test/testnextcloudpropagator.cpp +++ b/test/testnextcloudpropagator.cpp @@ -9,6 +9,7 @@ #include "propagatedownload.h" #include "owncloudpropagator_p.h" +#include "syncenginetestutils.h" using namespace OCC; namespace OCC { @@ -76,6 +77,28 @@ private slots: QCOMPARE(parseEtag(test.first), QByteArray(test.second)); } } + + void testParseException() + { + QNetworkRequest request; + request.setUrl(QStringLiteral("http://cloud.example.de/")); + const auto body = QByteArrayLiteral( + "\n" + "\n" + "Sabre\\Exception\\UnsupportedMediaType\n" + "Virus detected!\n" + ""); + const auto reply = new FakeErrorReply(QNetworkAccessManager::PutOperation, + request, + this, + 415, body); + const auto exceptionParsed = OCC::getExceptionFromReply(reply); + // verify parsing succeeded + QVERIFY(!exceptionParsed.first.isEmpty()); + QVERIFY(!exceptionParsed.second.isEmpty()); + // verify buffer is not changed + QCOMPARE(reply->readAll().size(), body.size()); + } }; QTEST_APPLESS_MAIN(TestNextcloudPropagator) From dfacf7af03bcbda02fc09feab0709bd592920fb0 Mon Sep 17 00:00:00 2001 From: alex-z Date: Thu, 30 Nov 2023 17:34:09 +0100 Subject: [PATCH 9/9] Added error reporting for E2EE issues. Signed-off-by: alex-z --- src/gui/tray/NCBusyIndicator.qml | 2 +- src/libsync/CMakeLists.txt | 7 +- src/libsync/account.cpp | 10 +- src/libsync/account.h | 4 +- src/libsync/clientsideencryption.cpp | 9 + src/libsync/clientstatusreporting.cpp | 408 +----------------- src/libsync/clientstatusreporting.h | 83 +--- src/libsync/clientstatusreportingcommon.cpp | 63 +++ src/libsync/clientstatusreportingcommon.h | 38 ++ src/libsync/clientstatusreportingdatabase.cpp | 280 ++++++++++++ src/libsync/clientstatusreportingdatabase.h | 68 +++ src/libsync/clientstatusreportingnetwork.cpp | 198 +++++++++ src/libsync/clientstatusreportingnetwork.h | 68 +++ src/libsync/clientstatusreportingrecord.cpp | 24 -- src/libsync/clientstatusreportingrecord.h | 5 +- src/libsync/discovery.cpp | 4 +- src/libsync/networkjobs.h | 7 + src/libsync/owncloudpropagator.cpp | 27 +- src/libsync/owncloudpropagator_p.h | 4 +- src/libsync/propagatedownload.cpp | 4 +- src/libsync/syncengine.cpp | 6 +- src/libsync/vfs/cfapi/vfs_cfapi.cpp | 2 +- test/testclientstatusreporting.cpp | 60 +-- 23 files changed, 835 insertions(+), 546 deletions(-) create mode 100644 src/libsync/clientstatusreportingcommon.cpp create mode 100644 src/libsync/clientstatusreportingcommon.h create mode 100644 src/libsync/clientstatusreportingdatabase.cpp create mode 100644 src/libsync/clientstatusreportingdatabase.h create mode 100644 src/libsync/clientstatusreportingnetwork.cpp create mode 100644 src/libsync/clientstatusreportingnetwork.h delete mode 100644 src/libsync/clientstatusreportingrecord.cpp diff --git a/src/gui/tray/NCBusyIndicator.qml b/src/gui/tray/NCBusyIndicator.qml index ddead28d4b07d..973d3dee1ed62 100644 --- a/src/gui/tray/NCBusyIndicator.qml +++ b/src/gui/tray/NCBusyIndicator.qml @@ -42,7 +42,7 @@ BusyIndicator { RotationAnimator { target: contentImage - running: false + running: root.running onRunningChanged: contentImage.rotation = 0 from: 0 to: 360 diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index bd7931a8670d3..cd124bd3b653b 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -26,8 +26,13 @@ set(libsync_SRCS clientproxy.cpp clientstatusreporting.h clientstatusreporting.cpp + clientstatusreportingcommon.h + clientstatusreportingcommon.cpp + clientstatusreportingdatabase.h + clientstatusreportingdatabase.cpp + clientstatusreportingnetwork.h + clientstatusreportingnetwork.cpp clientstatusreportingrecord.h - clientstatusreportingrecord.cpp cookiejar.h cookiejar.cpp discovery.h diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 1d27a74ffaca2..bea76c53b9069 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -286,19 +286,17 @@ void Account::setPushNotificationsReconnectInterval(int interval) void Account::trySetupClientStatusReporting() { - if (_capabilities.isClientStatusReportingEnabled()) { - if (!_clientStatusReporting) { - _clientStatusReporting.reset(new ClientStatusReporting(this)); - } + if (!_capabilities.isClientStatusReportingEnabled()) { + _clientStatusReporting.reset(); return; } if (!_clientStatusReporting) { - _clientStatusReporting.reset(); + _clientStatusReporting = std::make_unique(this); } } -void Account::reportClientStatus(const ClientStatusReporting::Status status) +void Account::reportClientStatus(const ClientStatusReportingStatus status) const { if (_clientStatusReporting) { _clientStatusReporting->reportClientStatus(status); diff --git a/src/libsync/account.h b/src/libsync/account.h index 1c51bb7a0c6b5..e118b53fb2f25 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -308,7 +308,7 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject void trySetupClientStatusReporting(); - void reportClientStatus(const ClientStatusReporting::Status status); + void reportClientStatus(const ClientStatusReportingStatus status) const; [[nodiscard]] std::shared_ptr userStatusConnector() const; @@ -444,7 +444,7 @@ private slots: PushNotifications *_pushNotifications = nullptr; - QScopedPointer _clientStatusReporting; + std::unique_ptr _clientStatusReporting; std::shared_ptr _userStatusConnector; diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 3278c450dfeba..dd9263b024d62 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1262,6 +1262,7 @@ bool ClientSideEncryption::sensitiveDataRemaining() const void ClientSideEncryption::failedToInitialize(const AccountPtr &account) { forgetSensitiveData(account); + account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); Q_EMIT initializationFinished(); } @@ -1775,6 +1776,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) if (metadataKeys.isEmpty()) { qCDebug(lcCse()) << "Could not migrate. No metadata keys found!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } @@ -1787,6 +1789,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) if (_metadataKey.isEmpty()) { qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } @@ -1861,6 +1864,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) } else { _metadataKey.clear(); _files.clear(); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } } @@ -1899,6 +1903,7 @@ QByteArray FolderMetadata::decryptData(const QByteArray &data) const if (decryptResult.isEmpty()) { qCDebug(lcCse()) << "ERROR. Could not decrypt the metadata key"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return {}; } return QByteArray::fromBase64(decryptResult); @@ -1916,6 +1921,7 @@ QByteArray FolderMetadata::decryptDataUsingKey(const QByteArray &data, if (decryptResult.isEmpty()) { qCDebug(lcCse()) << "ERROR. Could not decrypt"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return {}; } @@ -1979,6 +1985,7 @@ QByteArray FolderMetadata::encryptedMetadata() const { if (_metadataKey.isEmpty()) { qCDebug(lcCse) << "Metadata generation failed! Empty metadata key!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return {}; } const auto version = _account->capabilities().clientSideEncryptionVersion(); @@ -2000,6 +2007,7 @@ QByteArray FolderMetadata::encryptedMetadata() const { QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKey); if (encryptedEncrypted.isEmpty()) { + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); qCDebug(lcCse) << "Metadata generation failed!"; } QJsonObject file; @@ -2087,6 +2095,7 @@ bool FolderMetadata::moveFromFileDropToFiles() if (decryptedKey.isEmpty() || decryptedAuthenticationTag.isEmpty() || decryptedInitializationVector.isEmpty()) { qCDebug(lcCseMetadata) << "failed to decrypt filedrop entry" << it.key(); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); continue; } diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp index 617405047c5ec..682ff206e1c31 100644 --- a/src/libsync/clientstatusreporting.cpp +++ b/src/libsync/clientstatusreporting.cpp @@ -14,427 +14,59 @@ #include "clientstatusreporting.h" #include "account.h" +#include "clientstatusreportingdatabase.h" +#include "clientstatusreportingnetwork.h" #include "clientstatusreportingrecord.h" -#include -#include "common/c_jhash.h" -#include - -namespace -{ -constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; -constexpr auto statusNamesHash = "statusNamesHash"; - -constexpr auto statusReportCategoryE2eErrors = "e2e_errors"; -constexpr auto statusReportCategoryProblems = "problems"; -constexpr auto statusReportCategorySyncConflicts = "sync_conflicts"; -constexpr auto statusReportCategoryVirus = "virus_detected"; -} namespace OCC { Q_LOGGING_CATEGORY(lcClientStatusReporting, "nextcloud.sync.clientstatusreporting", QtInfoMsg) -ClientStatusReporting::ClientStatusReporting(Account *account, QObject *parent) - : QObject(parent) - , _account(account) -{ - init(); -} - -ClientStatusReporting::~ClientStatusReporting() +ClientStatusReporting::ClientStatusReporting(Account *account) { - if (_database.isOpen()) { - _database.close(); + for (int i = 0; i < static_cast(ClientStatusReportingStatus::Count); ++i) { + const auto statusString = clientStatusstatusStringFromNumber(static_cast(i)); + _statusStrings[i] = statusString; } -} -void ClientStatusReporting::init() -{ - Q_ASSERT(!_isInitialized); - if (_isInitialized) { - qCDebug(lcClientStatusReporting) << "Double call to init"; + if (_statusStrings.size() < static_cast(ClientStatusReportingStatus::Count)) { return; } - for (int i = 0; i < ClientStatusReporting::Status::Count; ++i) { - const auto statusString = statusStringFromNumber(static_cast(i)); - _statusNamesAndHashes[i] = {statusString, c_jhash64((uint8_t *)statusString.data(), statusString.size(), 0)}; - } - - const auto dbPath = makeDbPath(); - _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); - _database.setDatabaseName(dbPath); - - if (!_database.open()) { - qCDebug(lcClientStatusReporting) << "Could not setup client reporting, database connection error."; + _database = QSharedPointer::create(account); + if (!_database->isInitialized()) { return; } - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral( - "CREATE TABLE IF NOT EXISTS clientstatusreporting(" - "name VARCHAR(4096) PRIMARY KEY," - "status INTEGER(8)," - "count INTEGER," - "lastOccurrence INTEGER(8))")); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not setup client clientstatusreporting table:" << query.lastError().text(); + _reporter = std::make_unique(account, _database); + if (!_reporter->isInitialized()) { return; } - if (!query.prepare(QStringLiteral("CREATE INDEX IF NOT EXISTS name ON clientstatusreporting(name);")) || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not create index on clientstatusreporting table:" << query.lastError().text(); - return; - } - - if (!query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS keyvalue(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key))")) || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not setup client keyvalue table:" << query.lastError().text(); - return; - } - - // prevent issues in case enum gets changed in future, hash its value and clean the db in case there was a change - QByteArray statusNamesContatenated; - for (int i = 0; i < ClientStatusReporting::Status::Count; ++i) { - statusNamesContatenated += statusStringFromNumber(static_cast(i)); - } - statusNamesContatenated += QByteArray::number(ClientStatusReporting::Status::Count); - const auto statusNamesHashCurrent = QCryptographicHash::hash(statusNamesContatenated, QCryptographicHash::Md5).toHex(); - const auto statusNamesHashFromDb = getStatusNamesHash(); - - if (statusNamesHashCurrent != statusNamesHashFromDb) { - deleteClientStatusReportingRecords(); - setStatusNamesHash(statusNamesHashCurrent); - } - // - - _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval); - connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReporting::sendReportToServer); - _clientStatusReportingSendTimer.start(); - _isInitialized = true; } -QVector ClientStatusReporting::getClientStatusReportingRecords() const -{ - QVector records; - - QMutexLocker locker(&_mutex); - - QSqlQuery query; - if (!query.prepare(QStringLiteral("SELECT * FROM clientstatusreporting")) || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not get records from clientstatusreporting:" << query.lastError().text(); - return records; - } +ClientStatusReporting::~ClientStatusReporting() = default; - while (query.next()) { - ClientStatusReportingRecord record; - record._status = query.value(query.record().indexOf(QStringLiteral("status"))).toLongLong(); - record._name = query.value(query.record().indexOf(QStringLiteral("name"))).toByteArray(); - record._numOccurences = query.value(query.record().indexOf(QStringLiteral("count"))).toLongLong(); - record._lastOccurence = query.value(query.record().indexOf(QStringLiteral("lastOccurrence"))).toLongLong(); - records.push_back(record); - } - return records; -} - -void ClientStatusReporting::deleteClientStatusReportingRecords() const -{ - QSqlQuery query; - if (!query.prepare(QStringLiteral("DELETE FROM clientstatusreporting")) || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not delete records from clientstatusreporting:" << query.lastError().text(); - } -} - -Result ClientStatusReporting::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const -{ - Q_ASSERT(record.isValid()); - if (!record.isValid()) { - qCDebug(lcClientStatusReporting) << "Failed to set ClientStatusReportingRecord"; - return {QStringLiteral("Invalid parameter")}; - } - - const auto recordCopy = record; - - QMutexLocker locker(&_mutex); - - QSqlQuery query; - - const auto prepareResult = query.prepare( - QStringLiteral("INSERT OR REPLACE INTO clientstatusreporting (name, status, count, lastOccurrence) VALUES(:name, :status, :count, :lastOccurrence) ON CONFLICT(name) " - "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;")); - query.bindValue(QStringLiteral(":name"), recordCopy._name); - query.bindValue(QStringLiteral(":status"), recordCopy._status); - query.bindValue(QStringLiteral(":count"), 1); - query.bindValue(QStringLiteral(":lastOccurrence"), recordCopy._lastOccurence); - - if (!prepareResult || !query.exec()) { - const auto errorMessage = query.lastError().text(); - qCDebug(lcClientStatusReporting) << "Could not report client status:" << errorMessage; - return errorMessage; - } - - return {}; -} - -void ClientStatusReporting::reportClientStatus(const Status status) const +void ClientStatusReporting::reportClientStatus(const ClientStatusReportingStatus status) const { if (!_isInitialized) { - qCDebug(lcClientStatusReporting) << "Could not report status. Status reporting is not initialized"; return; } - Q_ASSERT(status >= 0 && status < Count); - if (status < 0 || status >= Status::Count) { - qCDebug(lcClientStatusReporting) << "Trying to report invalid status:" << status; + + Q_ASSERT(static_cast(status) >= 0 && static_cast(status) < static_cast(ClientStatusReportingStatus::Count)); + if (static_cast(status) < 0 || static_cast(status) >= static_cast(ClientStatusReportingStatus::Count)) { + qCDebug(lcClientStatusReporting) << "Trying to report invalid status:" << static_cast(status); return; } ClientStatusReportingRecord record; - record._name = _statusNamesAndHashes[status].first; - record._status = status; + record._name = _statusStrings[static_cast(status)]; + record._status = static_cast(status); record._lastOccurence = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); - const auto result = setClientStatusReportingRecord(record); + const auto result = _database->setClientStatusReportingRecord(record); if (!result.isValid()) { qCDebug(lcClientStatusReporting) << "Could not report client status:" << result.error(); } } - -void ClientStatusReporting::sendReportToServer() -{ - if (!_isInitialized) { - qCWarning(lcClientStatusReporting) << "Could not send report to server. Status reporting is not initialized"; - return; - } - - const auto lastSentReportTime = getLastSentReportTimestamp(); - if (QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - lastSentReportTime < repordSendIntervalMs) { - return; - } - - const auto report = prepareReport(); - if (report.isEmpty()) { - qCDebug(lcClientStatusReporting) << "Failed to generate report. Report is empty."; - return; - } - - const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); - clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report)); - clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); - connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json, int statusCode) { - if (statusCode == 0 || statusCode == 200 || statusCode == 201 || statusCode == 204) { - const auto metaFromJson = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("meta")).toObject(); - const auto codeFromJson = metaFromJson.value(QStringLiteral("statuscode")).toInt(); - if (codeFromJson == 0 || codeFromJson == 200 || codeFromJson == 201 || codeFromJson == 204) { - reportToServerSentSuccessfully(); - return; - } - qCDebug(lcClientStatusReporting) << "Received error when sending client report statusCode:" << statusCode << "codeFromJson:" << codeFromJson; - } - }); - clientStatusReportingJob->start(); -} - -void ClientStatusReporting::reportToServerSentSuccessfully() -{ - deleteClientStatusReportingRecords(); - setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()); -} - -QString ClientStatusReporting::makeDbPath() const -{ - if (!dbPathForTesting.isEmpty()) { - return dbPathForTesting; - } - const auto databaseId = QStringLiteral("%1@%2").arg(_account->davUser(), _account->url().toString()); - const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); - - return ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); -} - -quint64 ClientStatusReporting::getLastSentReportTimestamp() const -{ - QMutexLocker locker(&_mutex); - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); - query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; - return 0; - } - if (!query.next()) { - qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table:" << query.lastError().text(); - return 0; - } - return query.value(query.record().indexOf(QStringLiteral("value"))).toULongLong(); -} - -void ClientStatusReporting::setStatusNamesHash(const QByteArray &hash) const -{ - QMutexLocker locker(&_mutex); - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); - query.bindValue(QStringLiteral(":key"), statusNamesHash); - query.bindValue(QStringLiteral(":value"), hash); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not set status names hash."; - return; - } -} - -QByteArray ClientStatusReporting::getStatusNamesHash() const -{ - QMutexLocker locker(&_mutex); - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); - query.bindValue(QStringLiteral(":key"), statusNamesHash); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not get status names hash. No such record:" << statusNamesHash; - return {}; - } - if (!query.next()) { - qCDebug(lcClientStatusReporting) << "Could not get status names hash:" << query.lastError().text(); - return {}; - } - return query.value(query.record().indexOf(QStringLiteral("value"))).toByteArray(); -} - -QVariantMap ClientStatusReporting::prepareReport() const -{ - const auto records = getClientStatusReportingRecords(); - if (records.isEmpty()) { - return {}; - } - - QVariantMap report; - report[statusReportCategorySyncConflicts] = QVariantMap{}; - report[statusReportCategoryProblems] = QVariantMap{}; - report[statusReportCategoryVirus] = QVariantMap{}; - report[statusReportCategoryE2eErrors] = QVariantMap{}; - - QVariantMap e2eeErrors; - QVariantMap problems; - QVariantMap syncConflicts; - QVariantMap virusDetectedErrors; - - for (const auto &record : records) { - const auto categoryKey = classifyStatus(static_cast(record._status)); - - if (categoryKey.isEmpty()) { - qCDebug(lcClientStatusReporting) << "Could not classify status:"; - continue; - } - - if (categoryKey == statusReportCategoryE2eErrors) { - const auto initialCount = e2eeErrors[QStringLiteral("count")].toInt(); - e2eeErrors[QStringLiteral("count")] = initialCount + record._numOccurences; - e2eeErrors[QStringLiteral("oldest")] = record._lastOccurence; - report[categoryKey] = e2eeErrors; - } else if (categoryKey == statusReportCategoryProblems) { - problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}}; - report[categoryKey] = problems; - } else if (categoryKey == statusReportCategorySyncConflicts) { - const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); - syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; - syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; - report[categoryKey] = syncConflicts; - } else if (categoryKey == statusReportCategoryVirus) { - const auto initialCount = virusDetectedErrors[QStringLiteral("count")].toInt(); - virusDetectedErrors[QStringLiteral("count")] = initialCount + record._numOccurences; - virusDetectedErrors[QStringLiteral("oldest")] = record._lastOccurence; - report[categoryKey] = virusDetectedErrors; - } - } - return report; -} - -void ClientStatusReporting::setLastSentReportTimestamp(const quint64 timestamp) const -{ - QMutexLocker locker(&_mutex); - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); - query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); - query.bindValue(QStringLiteral(":value"), timestamp); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; - return; - } -} - -QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) -{ - Q_ASSERT(status >= 0 && status < Count); - if (status < 0 || status >= Status::Count) { - qCDebug(lcClientStatusReporting) << "Invalid status:" << status; - return {}; - } - - switch (status) { - case DownloadError_Cannot_Create_File: - return QByteArrayLiteral("DownloadResult.CANNOT_CREATE_FILE"); - case DownloadError_Conflict: - return QByteArrayLiteral("DownloadResult.CONFLICT"); - case DownloadError_ConflictCaseClash: - return QByteArrayLiteral("DownloadResult.CONFLICT_CASECLASH"); - case DownloadError_ConflictInvalidCharacters: - return QByteArrayLiteral("DownloadResult.CONFLICT_INVALID_CHARACTERS"); - case DownloadError_No_Free_Space: - return QByteArrayLiteral("DownloadResult.NO_FREE_SPACE"); - case DownloadError_ServerError: - return QByteArrayLiteral("DownloadResult.SERVER_ERROR"); - case DownloadError_Virtual_File_Hydration_Failure: - return QByteArrayLiteral("DownloadResult.VIRTUAL_FILE_HYDRATION_FAILURE"); - case E2EeError_GeneralError: - return QByteArrayLiteral("E2EeError.General"); - case UploadError_Conflict: - return QByteArrayLiteral("UploadResult.CONFLICT_CASECLASH"); - case UploadError_ConflictInvalidCharacters: - return QByteArrayLiteral("UploadResult.CONFLICT_INVALID_CHARACTERS"); - case UploadError_No_Free_Space: - return QByteArrayLiteral("UploadResult.NO_FREE_SPACE"); - case UploadError_No_Write_Permissions: - return QByteArrayLiteral("UploadResult.NO_WRITE_PERMISSIONS"); - case UploadError_ServerError: - return QByteArrayLiteral("UploadResult.SERVER_ERROR"); - case UploadError_Virus_Detected: - return QByteArrayLiteral("UploadResult.VIRUS_DETECTED"); - case Count: - return {}; - }; - return {}; -} - -QByteArray ClientStatusReporting::classifyStatus(const Status status) -{ - Q_ASSERT(status >= 0 && status < Count); - if (status < 0 || status >= Status::Count) { - qCDebug(lcClientStatusReporting) << "Invalid status:" << status; - return {}; - } - - switch (status) { - case DownloadError_Conflict: - case DownloadError_ConflictCaseClash: - case DownloadError_ConflictInvalidCharacters: - case UploadError_Conflict: - case UploadError_ConflictInvalidCharacters: - return statusReportCategorySyncConflicts; - case DownloadError_Cannot_Create_File: - case DownloadError_No_Free_Space: - case DownloadError_ServerError: - case DownloadError_Virtual_File_Hydration_Failure: - case UploadError_No_Free_Space: - case UploadError_No_Write_Permissions: - case UploadError_ServerError: - return statusReportCategoryProblems; - case UploadError_Virus_Detected: - return statusReportCategoryVirus; - case E2EeError_GeneralError: - return statusReportCategoryE2eErrors; - case Count: - return {}; - }; - return {}; -} -int ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000 * 60 * 2; // check if the time has come, every 2 minutes -quint64 ClientStatusReporting::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours -QString ClientStatusReporting::dbPathForTesting; } diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h index 70b7508c028b0..54aaf57853363 100644 --- a/src/libsync/clientstatusreporting.h +++ b/src/libsync/clientstatusreporting.h @@ -15,95 +15,38 @@ #include "owncloudlib.h" #include +#include "clientstatusreportingcommon.h" + +#include -#include #include #include -#include -#include -#include -#include -#include -#include +#include namespace OCC { class Account; +class ClientStatusReportingDatabase; +class ClientStatusReportingNetwork; struct ClientStatusReportingRecord; -class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject +class OWNCLOUDSYNC_EXPORT ClientStatusReporting { - Q_OBJECT public: - enum Status { - DownloadError_Cannot_Create_File = 0, - DownloadError_Conflict, - DownloadError_ConflictCaseClash, - DownloadError_ConflictInvalidCharacters, - DownloadError_No_Free_Space, - DownloadError_ServerError, - DownloadError_Virtual_File_Hydration_Failure, - E2EeError_GeneralError, - UploadError_Conflict, - UploadError_ConflictInvalidCharacters, - UploadError_No_Free_Space, - UploadError_No_Write_Permissions, - UploadError_ServerError, - UploadError_Virus_Detected, - Count, - }; - - explicit ClientStatusReporting(Account *account, QObject *parent = nullptr); - ~ClientStatusReporting() override; - - static QByteArray statusStringFromNumber(const Status status); + explicit ClientStatusReporting(Account *account); + ~ClientStatusReporting(); private: - void init(); // reporting must happen via Account - void reportClientStatus(const Status status) const; - - [[nodiscard]] Result setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const; - [[nodiscard]] QVector getClientStatusReportingRecords() const; - void deleteClientStatusReportingRecords() const; - - void setLastSentReportTimestamp(const quint64 timestamp) const; - [[nodiscard]] quint64 getLastSentReportTimestamp() const; - - void setStatusNamesHash(const QByteArray &hash) const; - [[nodiscard]] QByteArray getStatusNamesHash() const; - - [[nodiscard]] QVariantMap prepareReport() const; - void reportToServerSentSuccessfully(); - - [[nodiscard]] QString makeDbPath() const; - -private slots: - void sendReportToServer(); - -private: - static QByteArray classifyStatus(const Status status); - -public: - static int clientStatusReportingTrySendTimerInterval; - static quint64 repordSendIntervalMs; - // this must be set in unit tests on init - static QString dbPathForTesting; - -private: - - Account *_account = nullptr; - - QSqlDatabase _database; + void reportClientStatus(const ClientStatusReportingStatus status) const; bool _isInitialized = false; - QTimer _clientStatusReportingSendTimer; + QHash _statusStrings; - QHash> _statusNamesAndHashes; + QSharedPointer _database; - // inspired by SyncJournalDb - mutable QRecursiveMutex _mutex; + std::unique_ptr _reporter; friend class Account; }; diff --git a/src/libsync/clientstatusreportingcommon.cpp b/src/libsync/clientstatusreportingcommon.cpp new file mode 100644 index 0000000000000..fabbf2b0fbbaf --- /dev/null +++ b/src/libsync/clientstatusreportingcommon.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "clientstatusreportingcommon.h" +#include + +namespace OCC { +Q_LOGGING_CATEGORY(lcClientStatusReportingCommon, "nextcloud.sync.clientstatusreportingcommon", QtInfoMsg) + +QByteArray clientStatusstatusStringFromNumber(const ClientStatusReportingStatus status) +{ + Q_ASSERT(static_cast(status) >= 0 && static_cast(status) < static_cast(ClientStatusReportingStatus::Count)); + if (static_cast(status) < 0 || static_cast(status) >= static_cast(ClientStatusReportingStatus::Count)) { + qCDebug(lcClientStatusReportingCommon) << "Invalid status:" << static_cast(status); + return {}; + } + + switch (status) { + case ClientStatusReportingStatus::DownloadError_Cannot_Create_File: + return QByteArrayLiteral("DownloadResult.CANNOT_CREATE_FILE"); + case ClientStatusReportingStatus::DownloadError_Conflict: + return QByteArrayLiteral("DownloadResult.CONFLICT"); + case ClientStatusReportingStatus::DownloadError_ConflictCaseClash: + return QByteArrayLiteral("DownloadResult.CONFLICT_CASECLASH"); + case ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters: + return QByteArrayLiteral("DownloadResult.CONFLICT_INVALID_CHARACTERS"); + case ClientStatusReportingStatus::DownloadError_No_Free_Space: + return QByteArrayLiteral("DownloadResult.NO_FREE_SPACE"); + case ClientStatusReportingStatus::DownloadError_ServerError: + return QByteArrayLiteral("DownloadResult.SERVER_ERROR"); + case ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure: + return QByteArrayLiteral("DownloadResult.VIRTUAL_FILE_HYDRATION_FAILURE"); + case ClientStatusReportingStatus::E2EeError_GeneralError: + return QByteArrayLiteral("E2EeError.General"); + case ClientStatusReportingStatus::UploadError_Conflict: + return QByteArrayLiteral("UploadResult.CONFLICT_CASECLASH"); + case ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters: + return QByteArrayLiteral("UploadResult.CONFLICT_INVALID_CHARACTERS"); + case ClientStatusReportingStatus::UploadError_No_Free_Space: + return QByteArrayLiteral("UploadResult.NO_FREE_SPACE"); + case ClientStatusReportingStatus::UploadError_No_Write_Permissions: + return QByteArrayLiteral("UploadResult.NO_WRITE_PERMISSIONS"); + case ClientStatusReportingStatus::UploadError_ServerError: + return QByteArrayLiteral("UploadResult.SERVER_ERROR"); + case ClientStatusReportingStatus::UploadError_Virus_Detected: + return QByteArrayLiteral("UploadResult.VIRUS_DETECTED"); + case ClientStatusReportingStatus::Count: + return {}; + }; + return {}; +} +} diff --git a/src/libsync/clientstatusreportingcommon.h b/src/libsync/clientstatusreportingcommon.h new file mode 100644 index 0000000000000..4e08ca1efa6fd --- /dev/null +++ b/src/libsync/clientstatusreportingcommon.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "owncloudlib.h" +#include + +namespace OCC { +enum class ClientStatusReportingStatus { + DownloadError_Cannot_Create_File = 0, + DownloadError_Conflict, + DownloadError_ConflictCaseClash, + DownloadError_ConflictInvalidCharacters, + DownloadError_No_Free_Space, + DownloadError_ServerError, + DownloadError_Virtual_File_Hydration_Failure, + E2EeError_GeneralError, + UploadError_Conflict, + UploadError_ConflictInvalidCharacters, + UploadError_No_Free_Space, + UploadError_No_Write_Permissions, + UploadError_ServerError, + UploadError_Virus_Detected, + Count, +}; +QByteArray OWNCLOUDSYNC_EXPORT clientStatusstatusStringFromNumber(const ClientStatusReportingStatus status); +} diff --git a/src/libsync/clientstatusreportingdatabase.cpp b/src/libsync/clientstatusreportingdatabase.cpp new file mode 100644 index 0000000000000..169df5e13fffe --- /dev/null +++ b/src/libsync/clientstatusreportingdatabase.cpp @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "clientstatusreportingdatabase.h" + +#include "account.h" +#include + +#include +#include +#include + +namespace +{ +constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; +constexpr auto statusNamesHash = "statusNamesHash"; +} + +namespace OCC +{ +Q_LOGGING_CATEGORY(lcClientStatusReportingDatabase, "nextcloud.sync.clientstatusreportingdatabase", QtInfoMsg) + +ClientStatusReportingDatabase::ClientStatusReportingDatabase(const Account *account) +{ + const auto dbPath = makeDbPath(account); + _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); + _database.setDatabaseName(dbPath); + + if (!_database.open()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not setup client reporting, database connection error."; + return; + } + + QSqlQuery query; + const auto prepareResult = + query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS clientstatusreporting(" + "name VARCHAR(4096) PRIMARY KEY," + "status INTEGER(8)," + "count INTEGER," + "lastOccurrence INTEGER(8))")); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not setup client clientstatusreporting table:" << query.lastError().text(); + return; + } + + if (!query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS keyvalue(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key))")) || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not setup client keyvalue table:" << query.lastError().text(); + return; + } + + if (!updateStatusNamesHash()) { + return; + } + + _isInitialized = true; +} + +ClientStatusReportingDatabase::~ClientStatusReportingDatabase() +{ + if (_database.isOpen()) { + _database.close(); + } +} + +QVector ClientStatusReportingDatabase::getClientStatusReportingRecords() const +{ + QVector records; + + QMutexLocker locker(&_mutex); + + QSqlQuery query; + if (!query.prepare(QStringLiteral("SELECT * FROM clientstatusreporting")) || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get records from clientstatusreporting:" << query.lastError().text(); + return records; + } + + while (query.next()) { + ClientStatusReportingRecord record; + record._status = query.value(query.record().indexOf(QStringLiteral("status"))).toLongLong(); + record._name = query.value(query.record().indexOf(QStringLiteral("name"))).toByteArray(); + record._numOccurences = query.value(query.record().indexOf(QStringLiteral("count"))).toLongLong(); + record._lastOccurence = query.value(query.record().indexOf(QStringLiteral("lastOccurrence"))).toLongLong(); + records.push_back(record); + } + return records; +} + +Result ClientStatusReportingDatabase::deleteClientStatusReportingRecords() const +{ + QSqlQuery query; + if (!query.prepare(QStringLiteral("DELETE FROM clientstatusreporting")) || !query.exec()) { + const auto errorMessage = query.lastError().text(); + qCDebug(lcClientStatusReportingDatabase) << "Could not delete records from clientstatusreporting:" << errorMessage; + return errorMessage; + } + return {}; +} + +Result ClientStatusReportingDatabase::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const +{ + Q_ASSERT(record.isValid()); + if (!record.isValid()) { + qCDebug(lcClientStatusReportingDatabase) << "Failed to set ClientStatusReportingRecord"; + return {QStringLiteral("Invalid parameter")}; + } + + const auto recordCopy = record; + + QMutexLocker locker(&_mutex); + + QSqlQuery query; + + const auto prepareResult = query.prepare( + QStringLiteral("INSERT OR REPLACE INTO clientstatusreporting (name, status, count, lastOccurrence) VALUES(:name, :status, :count, :lastOccurrence) ON CONFLICT(name) " + "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;")); + query.bindValue(QStringLiteral(":name"), recordCopy._name); + query.bindValue(QStringLiteral(":status"), recordCopy._status); + query.bindValue(QStringLiteral(":count"), 1); + query.bindValue(QStringLiteral(":lastOccurrence"), recordCopy._lastOccurence); + + if (!prepareResult || !query.exec()) { + const auto errorMessage = query.lastError().text(); + qCDebug(lcClientStatusReportingDatabase) << "Could not report client status:" << errorMessage; + return errorMessage; + } + + return {}; +} + +QString ClientStatusReportingDatabase::makeDbPath(const Account *account) const +{ + if (!dbPathForTesting.isEmpty()) { + return dbPathForTesting; + } + const auto databaseId = QStringLiteral("%1@%2").arg(account->davUser(), account->url().toString()); + const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); + + return ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); +} + +bool ClientStatusReportingDatabase::updateStatusNamesHash() const +{ + QByteArray statusNamesContatenated; + for (int i = 0; i < static_cast(ClientStatusReportingStatus::Count); ++i) { + statusNamesContatenated += clientStatusstatusStringFromNumber(static_cast(i)); + } + statusNamesContatenated += QByteArray::number(static_cast(ClientStatusReportingStatus::Count)); + const auto statusNamesHashCurrent = QCryptographicHash::hash(statusNamesContatenated, QCryptographicHash::Md5).toHex(); + const auto statusNamesHashFromDb = getStatusNamesHash(); + + if (statusNamesHashCurrent != statusNamesHashFromDb) { + auto result = deleteClientStatusReportingRecords(); + if (!result.isValid()) { + return false; + } + + result = setStatusNamesHash(statusNamesHashCurrent); + if (!result.isValid()) { + return false; + } + } + return true; +} + +QVector ClientStatusReportingDatabase::getTableColumns(const QString &table) const +{ + QVector columns; + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("PRAGMA table_info('%1');").arg(table)); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could get table columns" << query.lastError().text(); + return columns; + } + while (query.next()) { + columns.append(query.value(1).toByteArray()); + } + return columns; +} + +bool ClientStatusReportingDatabase::addColumn(const QString &tableName, const QString &columnName, const QString &dataType, const bool withIndex) const +{ + const auto columns = getTableColumns(tableName); + const auto latin1ColumnName = columnName.toLatin1(); + if (columns.indexOf(latin1ColumnName) == -1) { + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("ALTER TABLE %1 ADD COLUMN %2 %3;").arg(tableName, columnName, dataType)); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << QStringLiteral("Failed to update table %1 structure: add %2 column").arg(tableName, columnName) << query.lastError().text(); + return false; + } + + if (withIndex) { + const auto prepareResult = query.prepare(QStringLiteral("CREATE INDEX %1_%2 ON %1(%2);").arg(tableName, columnName)); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << QStringLiteral("Failed to update table %1 structure: create index %2 column").arg(tableName, columnName) << query.lastError().text(); + return false; + } + } + } + return true; +} + +quint64 ClientStatusReportingDatabase::getLastSentReportTimestamp() const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); + query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; + return 0; + } + if (!query.next()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get last sent report timestamp from keyvalue table:" << query.lastError().text(); + return 0; + } + return query.value(query.record().indexOf(QStringLiteral("value"))).toULongLong(); +} + +Result ClientStatusReportingDatabase::setStatusNamesHash(const QByteArray &hash) const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); + query.bindValue(QStringLiteral(":key"), statusNamesHash); + query.bindValue(QStringLiteral(":value"), hash); + if (!prepareResult || !query.exec()) { + const auto errorMessage = query.lastError().text(); + qCDebug(lcClientStatusReportingDatabase) << "Could not set status names hash." << errorMessage; + return errorMessage; + } + return {}; +} + +QByteArray ClientStatusReportingDatabase::getStatusNamesHash() const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); + query.bindValue(QStringLiteral(":key"), statusNamesHash); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get status names hash. No such record:" << statusNamesHash; + return {}; + } + if (!query.next()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get status names hash:" << query.lastError().text(); + return {}; + } + return query.value(query.record().indexOf(QStringLiteral("value"))).toByteArray(); +} + +bool ClientStatusReportingDatabase::isInitialized() const +{ + return _isInitialized; +} + +void ClientStatusReportingDatabase::setLastSentReportTimestamp(const quint64 timestamp) const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); + query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); + query.bindValue(QStringLiteral(":value"), timestamp); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; + return; + } +} +QString ClientStatusReportingDatabase::dbPathForTesting; +} diff --git a/src/libsync/clientstatusreportingdatabase.h b/src/libsync/clientstatusreportingdatabase.h new file mode 100644 index 0000000000000..d3ef3c7161de2 --- /dev/null +++ b/src/libsync/clientstatusreportingdatabase.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "owncloudlib.h" +#include +#include "clientstatusreportingcommon.h" +#include "clientstatusreportingrecord.h" + +#include +#include +#include +#include +#include +#include + +namespace OCC { + +class Account; + +class OWNCLOUDSYNC_EXPORT ClientStatusReportingDatabase +{ +public: + explicit ClientStatusReportingDatabase(const Account *account); + ~ClientStatusReportingDatabase(); + + [[nodiscard]] Result setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const; + [[nodiscard]] QVector getClientStatusReportingRecords() const; + [[nodiscard]] Result deleteClientStatusReportingRecords() const; + + void setLastSentReportTimestamp(const quint64 timestamp) const; + [[nodiscard]] quint64 getLastSentReportTimestamp() const; + + [[nodiscard]] Result setStatusNamesHash(const QByteArray &hash) const; + [[nodiscard]] QByteArray getStatusNamesHash() const; + + [[nodiscard]] bool isInitialized() const; + +private: + [[nodiscard]] QString makeDbPath(const Account *account) const; + [[nodiscard]] bool updateStatusNamesHash() const; + [[nodiscard]] QVector getTableColumns(const QString &table) const; + [[nodiscard]]bool addColumn(const QString &tableName, const QString &columnName, const QString &dataType, const bool withIndex = false) const; + +public: + // this must be set in unit tests on init + static QString dbPathForTesting; + +private: + QSqlDatabase _database; + + bool _isInitialized = false; + + // inspired by SyncJournalDb + mutable QRecursiveMutex _mutex; +}; +} diff --git a/src/libsync/clientstatusreportingnetwork.cpp b/src/libsync/clientstatusreportingnetwork.cpp new file mode 100644 index 0000000000000..bee15f37bb564 --- /dev/null +++ b/src/libsync/clientstatusreportingnetwork.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "clientstatusreportingnetwork.h" + +#include "account.h" +#include "clientstatusreportingdatabase.h" +#include "clientstatusreportingrecord.h" +#include + +namespace +{ +constexpr auto statusReportCategoryE2eErrors = "e2e_errors"; +constexpr auto statusReportCategoryProblems = "problems"; +constexpr auto statusReportCategorySyncConflicts = "sync_conflicts"; +constexpr auto statusReportCategoryVirus = "virus_detected"; +} + +namespace OCC +{ +Q_LOGGING_CATEGORY(lcClientStatusReportingNetwork, "nextcloud.sync.clientstatusreportingnetwork", QtInfoMsg) + +ClientStatusReportingNetwork::ClientStatusReportingNetwork(Account *account, const QSharedPointer database, QObject *parent) + : QObject(parent) + , _account(account) + , _database(database) +{ + init(); +} + +ClientStatusReportingNetwork::~ClientStatusReportingNetwork() +{ +} + +void ClientStatusReportingNetwork::init() +{ + Q_ASSERT(!_isInitialized); + if (_isInitialized) { + return; + } + + _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval); + connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReportingNetwork::sendReportToServer); + _clientStatusReportingSendTimer.start(); + + _isInitialized = true; +} + +bool ClientStatusReportingNetwork::isInitialized() const +{ + return _isInitialized; +} + +void ClientStatusReportingNetwork::sendReportToServer() +{ + if (!_isInitialized) { + qCWarning(lcClientStatusReportingNetwork) << "Could not send report to server. Status reporting is not initialized"; + return; + } + + const auto lastSentReportTime = _database->getLastSentReportTimestamp(); + if (QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - lastSentReportTime < repordSendIntervalMs) { + return; + } + + const auto report = prepareReport(); + if (report.isEmpty()) { + return; + } + + if (!_account) { + return; + } + + const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); + clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report)); + clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); + connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json, int statusCode) { + const auto isSuccess = statusCode == HttpErrorCodeNone || statusCode == HttpErrorCodeSuccess || statusCode == HttpErrorCodeSuccessCreated + || statusCode == HttpErrorCodeSuccessNoContent; + if (isSuccess) { + const auto metaFromJson = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("meta")).toObject(); + const auto codeFromJson = metaFromJson.value(QStringLiteral("statuscode")).toInt(); + if (codeFromJson == HttpErrorCodeNone || codeFromJson == HttpErrorCodeSuccess || codeFromJson == HttpErrorCodeSuccessCreated + || codeFromJson == HttpErrorCodeSuccessNoContent) { + reportToServerSentSuccessfully(); + return; + } + qCDebug(lcClientStatusReportingNetwork) << "Received error when sending client report statusCode:" << statusCode << "codeFromJson:" << codeFromJson; + } + }); + clientStatusReportingJob->start(); +} + +void ClientStatusReportingNetwork::reportToServerSentSuccessfully() +{ + qCInfo(lcClientStatusReportingNetwork) << "Report sent successfully"; + if (!_database->deleteClientStatusReportingRecords()) { + qCDebug(lcClientStatusReportingNetwork) << "Could not delete records after sending the report"; + } + _database->setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()); +} + +QVariantMap ClientStatusReportingNetwork::prepareReport() const +{ + const auto records = _database->getClientStatusReportingRecords(); + if (records.isEmpty()) { + return {}; + } + + QVariantMap report; + report[statusReportCategorySyncConflicts] = QVariantMap{}; + report[statusReportCategoryProblems] = QVariantMap{}; + report[statusReportCategoryVirus] = QVariantMap{}; + report[statusReportCategoryE2eErrors] = QVariantMap{}; + + QVariantMap e2eeErrors; + QVariantMap problems; + QVariantMap syncConflicts; + QVariantMap virusDetectedErrors; + + for (const auto &record : records) { + const auto categoryKey = classifyStatus(static_cast(record._status)); + + if (categoryKey.isEmpty()) { + qCDebug(lcClientStatusReportingNetwork) << "Could not classify status:"; + continue; + } + + if (categoryKey == statusReportCategoryE2eErrors) { + const auto initialCount = e2eeErrors[QStringLiteral("count")].toInt(); + e2eeErrors[QStringLiteral("count")] = initialCount + record._numOccurences; + e2eeErrors[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = e2eeErrors; + } else if (categoryKey == statusReportCategoryProblems) { + problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}}; + report[categoryKey] = problems; + } else if (categoryKey == statusReportCategorySyncConflicts) { + const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); + syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; + syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = syncConflicts; + } else if (categoryKey == statusReportCategoryVirus) { + const auto initialCount = virusDetectedErrors[QStringLiteral("count")].toInt(); + virusDetectedErrors[QStringLiteral("count")] = initialCount + record._numOccurences; + virusDetectedErrors[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = virusDetectedErrors; + } + } + return report; +} + +QByteArray ClientStatusReportingNetwork::classifyStatus(const ClientStatusReportingStatus status) +{ + Q_ASSERT(static_cast(status) >= 0 && static_cast(status) < static_cast(ClientStatusReportingStatus::Count)); + if (static_cast(status) < 0 || static_cast(status) >= static_cast(ClientStatusReportingStatus::Count)) { + qCDebug(lcClientStatusReportingNetwork) << "Invalid status:" << static_cast(status); + return {}; + } + + switch (status) { + case ClientStatusReportingStatus::DownloadError_Conflict: + case ClientStatusReportingStatus::DownloadError_ConflictCaseClash: + case ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters: + case ClientStatusReportingStatus::UploadError_Conflict: + case ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters: + return statusReportCategorySyncConflicts; + case ClientStatusReportingStatus::DownloadError_Cannot_Create_File: + case ClientStatusReportingStatus::DownloadError_No_Free_Space: + case ClientStatusReportingStatus::DownloadError_ServerError: + case ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure: + case ClientStatusReportingStatus::UploadError_No_Free_Space: + case ClientStatusReportingStatus::UploadError_No_Write_Permissions: + case ClientStatusReportingStatus::UploadError_ServerError: + return statusReportCategoryProblems; + case ClientStatusReportingStatus::UploadError_Virus_Detected: + return statusReportCategoryVirus; + case ClientStatusReportingStatus::E2EeError_GeneralError: + return statusReportCategoryE2eErrors; + case ClientStatusReportingStatus::Count: + return {}; + }; + return {}; +} + +int ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval = 1000 * 60 * 2; // check if the time has come, every 2 minutes +quint64 ClientStatusReportingNetwork::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours +} diff --git a/src/libsync/clientstatusreportingnetwork.h b/src/libsync/clientstatusreportingnetwork.h new file mode 100644 index 0000000000000..0b4d10c65c520 --- /dev/null +++ b/src/libsync/clientstatusreportingnetwork.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "owncloudlib.h" +#include +#include "clientstatusreportingcommon.h" + +#include +#include +#include +#include +#include +#include + +namespace OCC { + +class Account; +class ClientStatusReportingDatabase; +struct ClientStatusReportingRecord; + +class OWNCLOUDSYNC_EXPORT ClientStatusReportingNetwork : public QObject +{ + Q_OBJECT +public: + explicit ClientStatusReportingNetwork(Account *account, const QSharedPointer database, QObject *parent = nullptr); + ~ClientStatusReportingNetwork() override; + +private: + void init(); + + [[nodiscard]] QVariantMap prepareReport() const; + void reportToServerSentSuccessfully(); + +private slots: + void sendReportToServer(); + +public: + [[nodiscard]] bool isInitialized() const; + + static QByteArray classifyStatus(const ClientStatusReportingStatus status); + + static int clientStatusReportingTrySendTimerInterval; + static quint64 repordSendIntervalMs; + // this must be set in unit tests on init + static QString dbPathForTesting; + +private: + Account *_account = nullptr; + + QSharedPointer _database; + + bool _isInitialized = false; + + QTimer _clientStatusReportingSendTimer; +}; +} diff --git a/src/libsync/clientstatusreportingrecord.cpp b/src/libsync/clientstatusreportingrecord.cpp deleted file mode 100644 index 4d795de64b3eb..0000000000000 --- a/src/libsync/clientstatusreportingrecord.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2023 by Oleksandr Zolotov - * - * 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 "clientstatusreportingrecord.h" - -namespace OCC -{ - -bool ClientStatusReportingRecord::isValid() const -{ - return _status >= 0 && !_name.isEmpty() && _lastOccurence > 0; -} -} diff --git a/src/libsync/clientstatusreportingrecord.h b/src/libsync/clientstatusreportingrecord.h index 6595177ec0995..94f190a0ee210 100644 --- a/src/libsync/clientstatusreportingrecord.h +++ b/src/libsync/clientstatusreportingrecord.h @@ -30,6 +30,9 @@ struct OWNCLOUDSYNC_EXPORT ClientStatusReportingRecord { quint64 _numOccurences = 1; quint64 _lastOccurence = 0; - [[nodiscard]] bool isValid() const; + [[nodiscard]] inline bool isValid() const + { + return _status >= 0 && !_name.isEmpty() && _lastOccurence > 0; + } }; } diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index c316dd518aa40..dc56ec602b85e 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -1709,13 +1709,13 @@ bool ProcessDirectoryJob::checkPermissions(const OCC::SyncFileItemPtr &item) // No permissions set return true; } else if (item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) { - _discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions); + _discoveryData->_account->reportClientStatus(ClientStatusReportingStatus::UploadError_No_Write_Permissions); qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file; item->_instruction = CSYNC_INSTRUCTION_ERROR; item->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder"); return false; } else if (!item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) { - _discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions); + _discoveryData->_account->reportClientStatus(ClientStatusReportingStatus::UploadError_No_Write_Permissions); qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file; item->_instruction = CSYNC_INSTRUCTION_ERROR; item->_errorString = tr("Not allowed because you don't have permission to add files in that folder"); diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h index 4fc397a7a4129..b299bdb0aa387 100644 --- a/src/libsync/networkjobs.h +++ b/src/libsync/networkjobs.h @@ -30,6 +30,13 @@ class QDomDocument; namespace OCC { +constexpr auto HttpErrorCodeNone = 0; +constexpr auto HttpErrorCodeSuccess = 200; +constexpr auto HttpErrorCodeSuccessCreated = 201; +constexpr auto HttpErrorCodeSuccessNoContent = 204; +constexpr auto HttpErrorCodeBadRequest = 400; +constexpr auto HttpErrorCodeUnsupportedMediaType = 415; + struct HttpError { int code; // HTTP error code diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 39b42f7518589..118bf86d55630 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -342,33 +342,34 @@ void PropagateItemJob::reportClientStatuses() { if (_item->_status == SyncFileItem::Status::Conflict) { if (_item->_direction == SyncFileItem::Direction::Up) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Conflict); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_Conflict); } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict); } } else if (_item->_status == SyncFileItem::Status::FileNameClash) { if (_item->_direction == SyncFileItem::Direction::Up) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters); } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters); } } else if (_item->_status == SyncFileItem::Status::FileNameInvalidOnServer) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters); } else if (_item->_status == SyncFileItem::Status::FileNameInvalid) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); - } else if (_item->_httpErrorCode != 0 && _item->_httpErrorCode != 200 && _item->_httpErrorCode != 201 && _item->_httpErrorCode != 204) { + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters); + } else if (_item->_httpErrorCode != HttpErrorCodeNone && _item->_httpErrorCode != HttpErrorCodeSuccess && _item->_httpErrorCode != HttpErrorCodeSuccessCreated + && _item->_httpErrorCode != HttpErrorCodeSuccessNoContent) { if (_item->_direction == SyncFileItem::Up) { - const auto isCodeBadReqOrUnsupportedMediaType = (_item->_httpErrorCode == 400 || _item->_httpErrorCode == 415); + const auto isCodeBadReqOrUnsupportedMediaType = (_item->_httpErrorCode == HttpErrorCodeBadRequest || _item->_httpErrorCode == HttpErrorCodeUnsupportedMediaType); const auto isExceptionInfoPresent = !_item->_errorExceptionName.isEmpty() && !_item->_errorExceptionMessage.isEmpty(); if (isCodeBadReqOrUnsupportedMediaType && isExceptionInfoPresent && _item->_errorExceptionName.contains(QStringLiteral("UnsupportedMediaType")) && _item->_errorExceptionMessage.contains(QStringLiteral("virus"), Qt::CaseInsensitive)) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Virus_Detected); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_Virus_Detected); } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_ServerError); } } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ServerError); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ServerError); } } } @@ -954,7 +955,7 @@ bool OwncloudPropagator::createConflict(const SyncFileItemPtr &item, } _journal->setConflictRecord(conflictRecord); - account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); + account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict); // Create a new upload job if the new conflict file should be uploaded if (account()->capabilities().uploadConflictFiles()) { @@ -1027,7 +1028,7 @@ OCC::Optional OwncloudPropagator::createCaseClashConflict(const SyncFil } _journal->setCaseConflictRecord(conflictRecord); - account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictCaseClash); + account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictCaseClash); // Need a new sync to detect the created copy of the conflicting file _anotherSyncNeeded = true; diff --git a/src/libsync/owncloudpropagator_p.h b/src/libsync/owncloudpropagator_p.h index 7d0c121bb4af6..beb150648db87 100644 --- a/src/libsync/owncloudpropagator_p.h +++ b/src/libsync/owncloudpropagator_p.h @@ -47,8 +47,6 @@ inline bool fileIsStillChanging(const OCC::SyncFileItem &item) } namespace OCC { - - inline QByteArray getEtagFromReply(QNetworkReply *reply) { QByteArray ocEtag = parseEtag(reply->rawHeader("OC-ETag")); @@ -63,7 +61,7 @@ inline QByteArray getEtagFromReply(QNetworkReply *reply) return ret; } -inline QPair getExceptionFromReply(QNetworkReply *reply) +inline QPair getExceptionFromReply(QNetworkReply * const reply) { Q_ASSERT(reply); if (!reply) { diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index b78adaae1c46c..afdf33d491b42 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -675,7 +675,7 @@ void PropagateDownloadFile::startDownload() if (_tmpFile.exists()) FileSystem::setFileReadOnly(_tmpFile.fileName(), false); if (!_tmpFile.open(QIODevice::Append | QIODevice::Unbuffered)) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Cannot_Create_File); qCWarning(lcPropagateDownload) << "could not open temporary file" << _tmpFile.fileName(); done(SyncFileItem::NormalError, _tmpFile.errorString(), ErrorCategory::GenericError); return; @@ -1260,7 +1260,7 @@ void PropagateDownloadFile::downloadFinished() emit propagator()->touchedFile(filename); // The fileChanged() check is done above to generate better error messages. if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), filename, &error)) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Cannot_Create_File); qCWarning(lcPropagateDownload) << QString("Rename failed: %1 => %2").arg(_tmpFile.fileName()).arg(filename); // If the file is locked, we want to retry this sync when it // becomes available again, otherwise try again directly diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index ed2e09d028dd9..523f3506a193b 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -314,7 +314,7 @@ void SyncEngine::conflictRecordMaintenance() } _journal->setConflictRecord(record); - account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); + account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict); } } } @@ -1262,7 +1262,7 @@ void SyncEngine::slotSummaryError(const QString &message) void SyncEngine::slotInsufficientLocalStorage() { - account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_No_Free_Space); + account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_No_Free_Space); slotSummaryError( tr("Disk space is low: Downloads that would reduce free space " "below %1 were skipped.") @@ -1271,7 +1271,7 @@ void SyncEngine::slotInsufficientLocalStorage() void SyncEngine::slotInsufficientRemoteStorage() { - account()->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Free_Space); + account()->reportClientStatus(ClientStatusReportingStatus::UploadError_No_Free_Space); auto msg = tr("There is insufficient space available on the server for some uploads."); if (_uniqueErrors.contains(msg)) return; diff --git a/src/libsync/vfs/cfapi/vfs_cfapi.cpp b/src/libsync/vfs/cfapi/vfs_cfapi.cpp index 4a5a958eeabc5..935cd8605fe4f 100644 --- a/src/libsync/vfs/cfapi/vfs_cfapi.cpp +++ b/src/libsync/vfs/cfapi/vfs_cfapi.cpp @@ -464,7 +464,7 @@ void VfsCfApi::onHydrationJobFinished(HydrationJob *job) qCInfo(lcCfApi) << "Hydration job finished" << job->requestId() << job->folderPath() << job->status(); emit hydrationRequestFinished(job->requestId()); if (!job->errorString().isEmpty()) { - params().account->reportClientStatus(ClientStatusReporting::Status::DownloadError_Virtual_File_Hydration_Failure); + params().account->reportClientStatus(ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure); emit failureHydrating(job->errorCode(), job->statusCode(), job->errorString(), job->folderPath()); } } diff --git a/test/testclientstatusreporting.cpp b/test/testclientstatusreporting.cpp index 5686baaa7391c..b018f20336666 100644 --- a/test/testclientstatusreporting.cpp +++ b/test/testclientstatusreporting.cpp @@ -13,7 +13,9 @@ */ #include "account.h" #include "accountstate.h" -#include "clientstatusreporting.h" +#include "clientstatusreportingcommon.h" +#include "clientstatusreportingdatabase.h" +#include "clientstatusreportingnetwork.h" #include "syncenginetestutils.h" #include @@ -39,8 +41,8 @@ class TestClientStatusReporting : public QObject private slots: void initTestCase() { - OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000; - OCC::ClientStatusReporting::repordSendIntervalMs = 2000; + OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval = 1000; + OCC::ClientStatusReportingNetwork::repordSendIntervalMs = 2000; fakeQnam.reset(new FakeQNAM({})); account = OCC::Account::create().get(); @@ -53,7 +55,7 @@ private slots: const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); dbFilePath = QDir::tempPath() + QStringLiteral("/.tests_userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); QFile(dbFilePath).remove(); - OCC::ClientStatusReporting::dbPathForTesting = dbFilePath; + OCC::ClientStatusReportingDatabase::dbPathForTesting = dbFilePath; QVariantMap capabilities; capabilities[QStringLiteral("security_guard")] = QVariantMap{ @@ -74,42 +76,42 @@ private slots: { for (int i = 0; i < 2; ++i) { // 5 conflicts - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Conflict); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_Conflict); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ConflictCaseClash); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Conflict); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_Conflict); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ConflictCaseClash); // 4 problems - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_ServerError); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ServerError); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_Virtual_File_Hydration_Failure); - // 3 occurances of UploadError_No_Write_Permissions - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); - - // 3 occurances of UploadError_Virus_Detected - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_ServerError); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ServerError); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure); + // 3 occurances of case ClientStatusReportingStatus::UploadError_No_Write_Permissions + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions); + + // 3 occurances of case ClientStatusReportingStatus::UploadError_Virus_Detected + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Virus_Detected); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Virus_Detected); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Virus_Detected); // 2 occurances of E2EeError_GeneralError - account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); - account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); - QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs); + account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + QTest::qWait(OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReportingNetwork::repordSendIntervalMs); QVERIFY(!bodyReceivedAndParsed.isEmpty()); // we must have 2 e2ee errors const auto virusDetectedErrorsReceived = bodyReceivedAndParsed.value("virus_detected").toMap(); QVERIFY(!virusDetectedErrorsReceived.isEmpty()); - QVERIFY(virusDetectedErrorsReceived.size(), 3); + QCOMPARE(virusDetectedErrorsReceived.value("count"), 3); // we must have 2 e2ee errors const auto e2eeErrorsReceived = bodyReceivedAndParsed.value("e2e_errors").toMap(); QVERIFY(!e2eeErrorsReceived.isEmpty()); - QVERIFY(e2eeErrorsReceived.size(), 2); + QCOMPARE(e2eeErrorsReceived.value("count"), 2); // we must have 5 conflicts const auto conflictsReceived = bodyReceivedAndParsed.value("sync_conflicts").toMap(); @@ -120,8 +122,8 @@ private slots: const auto problemsReceived = bodyReceivedAndParsed.value("problems").toMap(); QVERIFY(!problemsReceived.isEmpty()); QCOMPARE(problemsReceived.size(), 4); - const auto problemsNoWritePermissions = problemsReceived.value(OCC::ClientStatusReporting::statusStringFromNumber(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions)).toMap(); - // among those, 3 occurances of UploadError_No_Write_Permissions + const auto problemsNoWritePermissions = problemsReceived.value(OCC::clientStatusstatusStringFromNumber(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions)).toMap(); + // among those, 3 occurances of case ClientStatusReportingStatus::UploadError_No_Write_Permissions QCOMPARE(problemsNoWritePermissions.value("count"), 3); bodyReceivedAndParsed.clear(); @@ -130,7 +132,7 @@ private slots: void testNothingReportedAndNothingSent() { - QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs); + QTest::qWait(OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReportingNetwork::repordSendIntervalMs); QVERIFY(bodyReceivedAndParsed.isEmpty()); }