File indexing completed on 2024-12-15 03:45:04

0001 /*
0002     SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include <kuserfeedback_version.h>
0008 
0009 #include "logging_p.h"
0010 #include "provider.h"
0011 #include "provider_p.h"
0012 #include "abstractdatasource.h"
0013 #include "startcountsource.h"
0014 #include "surveyinfo.h"
0015 #include "usagetimesource.h"
0016 
0017 #include <common/surveytargetexpressionparser.h>
0018 #include <common/surveytargetexpressionevaluator.h>
0019 
0020 #include <QCoreApplication>
0021 #include <QDebug>
0022 #include <QDir>
0023 #include <QJsonArray>
0024 #include <QJsonDocument>
0025 #include <QJsonObject>
0026 #include <QStandardPaths>
0027 #include <QMetaEnum>
0028 #include <QNetworkAccessManager>
0029 #include <QNetworkReply>
0030 #include <QNetworkRequest>
0031 #include <QSettings>
0032 #include <QUuid>
0033 
0034 #include <algorithm>
0035 #include <numeric>
0036 
0037 using namespace KUserFeedback;
0038 
0039 ProviderPrivate::ProviderPrivate(Provider *qq)
0040     : q(qq)
0041     , networkAccessManager(nullptr)
0042     , redirectCount(0)
0043     , submissionInterval(-1)
0044     , telemetryMode(Provider::NoTelemetry)
0045     , surveyInterval(-1)
0046     , startCount(0)
0047     , usageTime(0)
0048     , encouragementStarts(-1)
0049     , encouragementTime(-1)
0050     , encouragementDelay(300)
0051     , encouragementInterval(-1)
0052     , backoffIntervalMinutes(-1)
0053 {
0054     submissionTimer.setSingleShot(true);
0055     QObject::connect(&submissionTimer, &QTimer::timeout, q, &Provider::submit);
0056 
0057     startTime.start();
0058 
0059     encouragementTimer.setSingleShot(true);
0060     QObject::connect(&encouragementTimer, &QTimer::timeout, q, [this]() { emitShowEncouragementMessage(); });
0061 }
0062 
0063 ProviderPrivate::~ProviderPrivate()
0064 {
0065     qDeleteAll(dataSources);
0066 }
0067 
0068 int ProviderPrivate::currentApplicationTime() const
0069 {
0070     return usageTime + (startTime.elapsed() / 1000);
0071 }
0072 
0073 static QMetaEnum telemetryModeEnum()
0074 {
0075     const auto idx = Provider::staticMetaObject.indexOfEnumerator("TelemetryMode");
0076     Q_ASSERT(idx >= 0);
0077     return Provider::staticMetaObject.enumerator(idx);
0078 }
0079 
0080 std::unique_ptr<QSettings> ProviderPrivate::makeSettings() const
0081 {
0082     // attempt to put our settings next to the application ones,
0083     // so replicate how QSettings handles this
0084     auto org =
0085 #ifdef Q_OS_MAC
0086         QCoreApplication::organizationDomain().isEmpty() ? QCoreApplication::organizationName() : QCoreApplication::organizationDomain();
0087 #else
0088         QCoreApplication::organizationName().isEmpty() ? QCoreApplication::organizationDomain() : QCoreApplication::organizationName();
0089 #endif
0090     if (org.isEmpty())
0091         org = QLatin1String("Unknown Organization");
0092 
0093     std::unique_ptr<QSettings> s(new QSettings(org, QStringLiteral("UserFeedback.") + productId));
0094     return s;
0095 }
0096 
0097 std::unique_ptr<QSettings> ProviderPrivate::makeGlobalSettings() const
0098 {
0099     const auto org =
0100 #ifdef Q_OS_MAC
0101         QStringLiteral("kde.org");
0102 #else
0103         QStringLiteral("KDE");
0104 #endif
0105     std::unique_ptr<QSettings> s(new QSettings(org, QStringLiteral("UserFeedback")));
0106     return s;
0107 }
0108 
0109 void ProviderPrivate::load()
0110 {
0111     auto s = makeSettings();
0112     s->beginGroup(QStringLiteral("UserFeedback"));
0113     lastSubmitTime = s->value(QStringLiteral("LastSubmission")).toDateTime();
0114 
0115     const auto modeStr = s->value(QStringLiteral("StatisticsCollectionMode")).toByteArray();
0116     telemetryMode = static_cast<Provider::TelemetryMode>(std::max(telemetryModeEnum().keyToValue(modeStr.constData()), 0));
0117 
0118     surveyInterval = s->value(QStringLiteral("SurveyInterval"), -1).toInt();
0119     lastSurveyTime = s->value(QStringLiteral("LastSurvey")).toDateTime();
0120     completedSurveys = s->value(QStringLiteral("CompletedSurveys"), QStringList()).toStringList();
0121 
0122     startCount = std::max(s->value(QStringLiteral("ApplicationStartCount"), 0).toInt(), 0);
0123     usageTime = std::max(s->value(QStringLiteral("ApplicationTime"), 0).toInt(), 0);
0124 
0125     lastEncouragementTime = s->value(QStringLiteral("LastEncouragement")).toDateTime();
0126 
0127     s->endGroup();
0128 
0129     // ensure consistent times if settings is corrupt, to avoid overflows later in the code
0130     const auto now = QDateTime::currentDateTime();
0131     if (now < lastSubmitTime) {
0132         lastSubmitTime = now;
0133     }
0134     if (now < lastSurveyTime) {
0135         lastSurveyTime = now;
0136     }
0137     if (now < lastEncouragementTime) {
0138         lastEncouragementTime = now;
0139     }
0140 
0141     foreach (auto source, dataSources) {
0142         s->beginGroup(QStringLiteral("Source-") + source->id());
0143         source->load(s.get());
0144         s->endGroup();
0145     }
0146 
0147     auto g = makeGlobalSettings();
0148     g->beginGroup(QStringLiteral("UserFeedback"));
0149     lastSurveyTime = std::max(g->value(QStringLiteral("LastSurvey")).toDateTime(), lastSurveyTime);
0150     lastEncouragementTime = std::max(g->value(QStringLiteral("LastEncouragement")).toDateTime(), lastEncouragementTime);
0151 }
0152 
0153 void ProviderPrivate::store()
0154 {
0155     auto s = makeSettings();
0156     s->beginGroup(QStringLiteral("UserFeedback"));
0157 
0158     // another process might have changed this, so read the base value first before writing
0159     usageTime = std::max(s->value(QStringLiteral("ApplicationTime"), 0).toInt(), usageTime);
0160     s->setValue(QStringLiteral("ApplicationTime"), currentApplicationTime());
0161     usageTime = currentApplicationTime();
0162     startTime.restart();
0163 
0164     s->endGroup();
0165 
0166     foreach (auto source, dataSources) {
0167         s->beginGroup(QStringLiteral("Source-") + source->id());
0168         source->store(s.get());
0169         s->endGroup();
0170     }
0171 }
0172 
0173 void ProviderPrivate::storeOne(const QString &key, const QVariant &value)
0174 {
0175     auto s = makeSettings();
0176     s->beginGroup(QStringLiteral("UserFeedback"));
0177     s->setValue(key, value);
0178 }
0179 
0180 void ProviderPrivate::storeOneGlobal(const QString &key, const QVariant &value)
0181 {
0182     auto s = makeGlobalSettings();
0183     s->beginGroup(QStringLiteral("UserFeedback"));
0184     s->setValue(key, value);
0185 }
0186 
0187 void ProviderPrivate::aboutToQuit()
0188 {
0189     store();
0190 }
0191 
0192 bool ProviderPrivate::isValidSource(AbstractDataSource *source) const
0193 {
0194     if (source->id().isEmpty()) {
0195         qCWarning(Log) << "Skipping data source with empty name!";
0196         return false;
0197     }
0198     if (source->telemetryMode() == Provider::NoTelemetry) {
0199         qCWarning(Log) << "Source" << source->id() << "attempts to report data unconditionally, ignoring!";
0200         return false;
0201     }
0202     if (source->description().isEmpty()) {
0203         qCWarning(Log) << "Source" << source->id() << "has no description, ignoring!";
0204         return false;
0205     }
0206 
0207     Q_ASSERT(!source->id().isEmpty());
0208     Q_ASSERT(source->telemetryMode() != Provider::NoTelemetry);
0209     Q_ASSERT(!source->description().isEmpty());
0210     return true;
0211 }
0212 
0213 QByteArray ProviderPrivate::jsonData(Provider::TelemetryMode mode) const
0214 {
0215     QJsonObject obj;
0216     if (mode != Provider::NoTelemetry) {
0217         foreach (auto source, dataSources) {
0218             if (!isValidSource(source) || !source->isActive())
0219                 continue;
0220             if (mode < source->telemetryMode())
0221                 continue;
0222             const auto data = source->data();
0223             if (data.canConvert<QVariantMap>())
0224                 obj.insert(source->id(), QJsonObject::fromVariantMap(data.toMap()));
0225             else if (data.canConvert<QVariantList>())
0226                 obj.insert(source->id(), QJsonArray::fromVariantList(data.value<QVariantList>()));
0227             else
0228                 qCWarning(Log) << "wrong type for" << source->id() << data;
0229         }
0230     }
0231 
0232     QJsonDocument doc(obj);
0233     return doc.toJson();
0234 }
0235 
0236 void ProviderPrivate::scheduleNextSubmission(qint64 minTime)
0237 {
0238     submissionTimer.stop();
0239     if (!q->isEnabled())
0240         return;
0241     if (submissionInterval <= 0 || (telemetryMode == Provider::NoTelemetry && surveyInterval < 0))
0242         return;
0243 
0244     if (minTime == 0) {
0245         // If this is a regularly scheduled submission reset the backoff
0246         backoffIntervalMinutes = -1;
0247     }
0248 
0249     Q_ASSERT(submissionInterval > 0);
0250 
0251     const auto nextSubmission = lastSubmitTime.addDays(submissionInterval);
0252     const auto now = QDateTime::currentDateTime();
0253     submissionTimer.start(std::max(minTime, now.msecsTo(nextSubmission)));
0254 }
0255 
0256 void ProviderPrivate::submitFinished(QNetworkReply *reply)
0257 {
0258     reply->deleteLater();
0259 
0260     if (reply->error() != QNetworkReply::NoError) {
0261         if (backoffIntervalMinutes == -1) {
0262             backoffIntervalMinutes = 2;
0263         } else {
0264             backoffIntervalMinutes = backoffIntervalMinutes * 2;
0265         }
0266         qCWarning(Log) << "failed to submit user feedback:" << reply->errorString() << reply->readAll() << ". Calling scheduleNextSubmission with minTime" << backoffIntervalMinutes << "minutes";
0267         scheduleNextSubmission(backoffIntervalMinutes * 60000ll);
0268         return;
0269     }
0270 
0271     const auto redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
0272     if (redirectTarget.isValid()) {
0273         if (++redirectCount >= 20) {
0274             qCWarning(Log) << "Redirect loop on" << reply->url().resolved(redirectTarget).toString();
0275             return;
0276         }
0277         submit(reply->url().resolved(redirectTarget));
0278         return;
0279     }
0280 
0281     lastSubmitTime = QDateTime::currentDateTime();
0282 
0283     auto s = makeSettings();
0284     s->beginGroup(QStringLiteral("UserFeedback"));
0285     s->setValue(QStringLiteral("LastSubmission"), lastSubmitTime);
0286     s->endGroup();
0287 
0288     writeAuditLog(lastSubmitTime);
0289 
0290     // reset source counters
0291     foreach (auto source, dataSources) {
0292         s->beginGroup(QStringLiteral("Source-") + source->id());
0293         source->reset(s.get());
0294         s->endGroup();
0295     }
0296 
0297     const auto obj = QJsonDocument::fromJson(reply->readAll()).object();
0298     const auto it = obj.find(QLatin1String("surveys"));
0299     if (it != obj.end() && surveyInterval >= 0) {
0300         const auto a = it.value().toArray();
0301         qCDebug(Log) << "received" << a.size() << "surveys";
0302         foreach(const auto &s, a) {
0303             const auto survey = SurveyInfo::fromJson(s.toObject());
0304             if (selectSurvey(survey))
0305                 break;
0306         }
0307     }
0308 
0309     scheduleNextSubmission();
0310 }
0311 
0312 QVariant ProviderPrivate::sourceData(const QString& sourceId) const
0313 {
0314     foreach (auto src, dataSources) {
0315         if (src->id() == sourceId)
0316             return src->data();
0317     }
0318     return QVariant();
0319 }
0320 
0321 bool ProviderPrivate::selectSurvey(const SurveyInfo &survey) const
0322 {
0323     qCDebug(Log) << "got survey:" << survey.url() << survey.target();
0324     if (!q->isEnabled() || !survey.isValid() || completedSurveys.contains(survey.uuid().toString()))
0325         return false;
0326 
0327     if (surveyInterval != 0 && lastSurveyTime.addDays(surveyInterval) > QDateTime::currentDateTime())
0328         return false;
0329 
0330     if (!survey.target().isEmpty()) {
0331         SurveyTargetExpressionParser parser;
0332         if (!parser.parse(survey.target())) {
0333             qCDebug(Log) << "failed to parse target expression";
0334             return false;
0335         }
0336 
0337         SurveyTargetExpressionEvaluator eval;
0338         eval.setDataProvider(this);
0339         if (!eval.evaluate(parser.expression()))
0340             return false;
0341     }
0342 
0343     qCDebug(Log) << "picked survey:" << survey.url();
0344     Q_EMIT q->surveyAvailable(survey);
0345     return true;
0346 }
0347 
0348 Provider::TelemetryMode ProviderPrivate::highestTelemetryMode() const
0349 {
0350     auto mode = Provider::NoTelemetry;
0351     foreach (auto src, dataSources)
0352         mode = std::max(mode, src->telemetryMode());
0353     return mode;
0354 }
0355 
0356 void ProviderPrivate::scheduleEncouragement()
0357 {
0358     encouragementTimer.stop();
0359     if (!q->isEnabled())
0360         return;
0361 
0362     // already done, not repetition
0363     if (lastEncouragementTime.isValid() && encouragementInterval <= 0)
0364         return;
0365 
0366     if (encouragementStarts < 0 && encouragementTime < 0) // encouragement disabled
0367         return;
0368 
0369     if (encouragementStarts > startCount) // we need more starts
0370         return;
0371 
0372     if (telemetryMode >= highestTelemetryMode() && surveyInterval == 0) // already everything enabled
0373         return;
0374     // no repetition if some feedback is enabled
0375     if (lastEncouragementTime.isValid() && (telemetryMode > Provider::NoTelemetry || surveyInterval >= 0))
0376         return;
0377 
0378     Q_ASSERT(encouragementDelay >= 0);
0379     int timeToEncouragement = encouragementDelay;
0380     if (encouragementTime > 0)
0381         timeToEncouragement = std::max(timeToEncouragement, encouragementTime - currentApplicationTime());
0382     if (lastEncouragementTime.isValid()) {
0383         Q_ASSERT(encouragementInterval > 0);
0384         const auto targetTime = lastEncouragementTime.addDays(encouragementInterval);
0385         timeToEncouragement = std::max(timeToEncouragement, (int)QDateTime::currentDateTime().secsTo(targetTime));
0386     }
0387     encouragementTimer.start(timeToEncouragement * 1000);
0388 }
0389 
0390 void ProviderPrivate::emitShowEncouragementMessage()
0391 {
0392     lastEncouragementTime = QDateTime::currentDateTime(); // TODO make this explicit, in case the host application decides to delay?
0393     storeOne(QStringLiteral("LastEncouragement"), lastEncouragementTime);
0394     storeOneGlobal(QStringLiteral("LastEncouragement"), lastEncouragementTime);
0395     Q_EMIT q->showEncouragementMessage();
0396 }
0397 
0398 
0399 Provider::Provider(QObject *parent) :
0400     QObject(parent),
0401     d(new ProviderPrivate(this))
0402 {
0403     qCDebug(Log);
0404 
0405     connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { d->aboutToQuit(); });
0406 
0407     auto domain = QCoreApplication::organizationDomain().split(QLatin1Char('.'));
0408     std::reverse(domain.begin(), domain.end());
0409     auto id = domain.join(QLatin1String("."));
0410     if (!id.isEmpty())
0411         id += QLatin1Char('.');
0412     id += QCoreApplication::applicationName();
0413     setProductIdentifier(id);
0414 }
0415 
0416 Provider::~Provider()
0417 {
0418     delete d;
0419 }
0420 
0421 bool Provider::isEnabled() const
0422 {
0423     auto s = d->makeGlobalSettings();
0424     s->beginGroup(QStringLiteral("UserFeedback"));
0425     return s->value(QStringLiteral("Enabled"), true).toBool();
0426 }
0427 
0428 void Provider::setEnabled(bool enabled)
0429 {
0430     if (enabled == isEnabled())
0431         return;
0432     d->storeOneGlobal(QStringLiteral("Enabled"), enabled);
0433     Q_EMIT enabledChanged();
0434 }
0435 
0436 void Provider::restoreDefaults()
0437 {
0438     setTelemetryMode(NoTelemetry);
0439     setSurveyInterval(-1);
0440 }
0441 
0442 QString Provider::productIdentifier() const
0443 {
0444     return d->productId;
0445 }
0446 
0447 void Provider::setProductIdentifier(const QString &productId)
0448 {
0449     Q_ASSERT(!productId.isEmpty());
0450     if (productId == d->productId)
0451         return;
0452     d->productId = productId;
0453 
0454     d->load();
0455     d->startCount++;
0456     d->storeOne(QStringLiteral("ApplicationStartCount"), d->startCount);
0457 
0458     Q_EMIT providerSettingsChanged();
0459 
0460     d->scheduleEncouragement();
0461     d->scheduleNextSubmission();
0462 }
0463 
0464 QUrl Provider::feedbackServer() const
0465 {
0466     return d->serverUrl;
0467 }
0468 
0469 void Provider::setFeedbackServer(const QUrl &url)
0470 {
0471     if (d->serverUrl == url)
0472         return;
0473     d->serverUrl = url;
0474     Q_EMIT providerSettingsChanged();
0475 }
0476 
0477 int Provider::submissionInterval() const
0478 {
0479     return d->submissionInterval;
0480 }
0481 
0482 void Provider::setSubmissionInterval(int days)
0483 {
0484     if (d->submissionInterval == days)
0485         return;
0486     d->submissionInterval = days;
0487     Q_EMIT providerSettingsChanged();
0488     d->scheduleNextSubmission();
0489 }
0490 
0491 Provider::TelemetryMode Provider::telemetryMode() const
0492 {
0493     return d->telemetryMode;
0494 }
0495 
0496 void Provider::setTelemetryMode(TelemetryMode mode)
0497 {
0498     if (d->telemetryMode == mode)
0499         return;
0500 
0501     d->telemetryMode = mode;
0502     d->storeOne(QStringLiteral("StatisticsCollectionMode"), QString::fromLatin1(telemetryModeEnum().valueToKey(d->telemetryMode)));
0503     d->scheduleNextSubmission();
0504     d->scheduleEncouragement();
0505     Q_EMIT telemetryModeChanged();
0506 }
0507 
0508 void Provider::addDataSource(AbstractDataSource *source)
0509 {
0510     // special cases for sources where we track the data here, as it's needed even if we don't report it
0511     if (auto countSrc = dynamic_cast<StartCountSource*>(source))
0512         countSrc->setProvider(d);
0513     if (auto timeSrc = dynamic_cast<UsageTimeSource*>(source))
0514         timeSrc->setProvider(d);
0515 
0516     d->dataSources.push_back(source);
0517     d->dataSourcesById[source->id()] = source;
0518 
0519     auto s = d->makeSettings();
0520     s->beginGroup(QStringLiteral("Source-") + source->id());
0521     source->load(s.get());
0522 
0523     Q_EMIT dataSourcesChanged();
0524 }
0525 
0526 QVector<AbstractDataSource*> Provider::dataSources() const
0527 {
0528     return d->dataSources;
0529 }
0530 
0531 AbstractDataSource *Provider::dataSource(const QString &id) const
0532 {
0533     auto it = d->dataSourcesById.find(id);
0534     return it != std::end(d->dataSourcesById) ? *it : nullptr;
0535 }
0536 
0537 int Provider::surveyInterval() const
0538 {
0539     return d->surveyInterval;
0540 }
0541 
0542 void Provider::setSurveyInterval(int days)
0543 {
0544     if (d->surveyInterval == days)
0545         return;
0546 
0547     d->surveyInterval = days;
0548     d->storeOne(QStringLiteral("SurveyInterval"), d->surveyInterval);
0549 
0550     d->scheduleNextSubmission();
0551     d->scheduleEncouragement();
0552     Q_EMIT surveyIntervalChanged();
0553 }
0554 
0555 int Provider::applicationStartsUntilEncouragement() const
0556 {
0557     return d->encouragementStarts;
0558 }
0559 
0560 void Provider::setApplicationStartsUntilEncouragement(int starts)
0561 {
0562     if (d->encouragementStarts == starts)
0563         return;
0564     d->encouragementStarts = starts;
0565     Q_EMIT providerSettingsChanged();
0566     d->scheduleEncouragement();
0567 }
0568 
0569 int Provider::applicationUsageTimeUntilEncouragement() const
0570 {
0571     return d->encouragementTime;
0572 }
0573 
0574 void Provider::setApplicationUsageTimeUntilEncouragement(int secs)
0575 {
0576     if (d->encouragementTime == secs)
0577         return;
0578     d->encouragementTime = secs;
0579     Q_EMIT providerSettingsChanged();
0580     d->scheduleEncouragement();
0581 }
0582 
0583 int Provider::encouragementDelay() const
0584 {
0585     return d->encouragementDelay;
0586 }
0587 
0588 void Provider::setEncouragementDelay(int secs)
0589 {
0590     if (d->encouragementDelay == secs)
0591         return;
0592     d->encouragementDelay = std::max(0, secs);
0593     Q_EMIT providerSettingsChanged();
0594     d->scheduleEncouragement();
0595 }
0596 
0597 int Provider::encouragementInterval() const
0598 {
0599     return d->encouragementInterval;
0600 }
0601 
0602 void Provider::setEncouragementInterval(int days)
0603 {
0604     if (d->encouragementInterval == days)
0605         return;
0606     d->encouragementInterval = days;
0607     Q_EMIT providerSettingsChanged();
0608     d->scheduleEncouragement();
0609 }
0610 
0611 void Provider::surveyCompleted(const SurveyInfo &info)
0612 {
0613     d->completedSurveys.push_back(info.uuid().toString());
0614     d->lastSurveyTime = QDateTime::currentDateTime();
0615 
0616     auto s = d->makeSettings();
0617     s->beginGroup(QStringLiteral("UserFeedback"));
0618     s->setValue(QStringLiteral("LastSurvey"), d->lastSurveyTime);
0619     s->setValue(QStringLiteral("CompletedSurveys"), d->completedSurveys);
0620 
0621     d->storeOneGlobal(QStringLiteral("LastSurvey"), d->lastSurveyTime);
0622 }
0623 
0624 void Provider::load()
0625 {
0626     d->load();
0627 }
0628 
0629 void Provider::store()
0630 {
0631     d->store();
0632 }
0633 
0634 void Provider::submit()
0635 {
0636     if (!isEnabled()) {
0637         qCWarning(Log) << "Global kill switch is enabled";
0638         return;
0639     }
0640     if (d->productId.isEmpty()) {
0641         qCWarning(Log) << "No productId specified!";
0642         return;
0643     }
0644     if (!d->serverUrl.isValid()) {
0645         qCWarning(Log) << "No feedback server URL specified!";
0646         return;
0647     }
0648 
0649     if (!d->networkAccessManager)
0650         d->networkAccessManager = new QNetworkAccessManager(this);
0651 
0652     auto url = d->serverUrl;
0653     auto path = d->serverUrl.path();
0654     if (!path.endsWith(QLatin1Char('/')))
0655         path += QLatin1Char('/');
0656     path += QStringLiteral("receiver/submit/") + d->productId;
0657     url.setPath(path);
0658     d->submitProbe(url);
0659 }
0660 
0661 void ProviderPrivate::submit(const QUrl &url)
0662 {
0663     QNetworkRequest request(url);
0664     request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
0665     request.setHeader(QNetworkRequest::UserAgentHeader, QString(QStringLiteral("KUserFeedback/") + QStringLiteral(KUSERFEEDBACK_VERSION_STRING)));
0666     auto reply = networkAccessManager->post(request, jsonData(telemetryMode));
0667     QObject::connect(reply, &QNetworkReply::finished, q, [this, reply]() { submitFinished(reply); });
0668 }
0669 
0670 void ProviderPrivate::submitProbe(const QUrl &url)
0671 {
0672     QNetworkRequest request(url);
0673     request.setHeader(QNetworkRequest::UserAgentHeader, QString(QStringLiteral("KUserFeedback/") + QStringLiteral(KUSERFEEDBACK_VERSION_STRING)));
0674     auto reply = networkAccessManager->get(request);
0675     QObject::connect(reply, &QNetworkReply::finished, q, [this, reply]() { submitProbeFinished(reply); });
0676 }
0677 
0678 void ProviderPrivate::submitProbeFinished(QNetworkReply *reply)
0679 {
0680     reply->deleteLater();
0681 
0682     if (reply->error() != QNetworkReply::NoError) {
0683         qCWarning(Log) << "failed to probe user feedback submission interface:" << reply->errorString() << reply->readAll();
0684         return;
0685     }
0686 
0687     const auto redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
0688     if (redirectTarget.isValid()) {
0689         if (++redirectCount >= 20) {
0690             qCWarning(Log) << "Redirect loop on" << reply->url().resolved(redirectTarget).toString();
0691             return;
0692         }
0693         submitProbe(reply->url().resolved(redirectTarget));
0694         return;
0695     }
0696 
0697     submit(reply->url());
0698 }
0699 
0700 void ProviderPrivate::writeAuditLog(const QDateTime &dt)
0701 {
0702     const QString path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QStringLiteral("/kuserfeedback/audit");
0703     QDir().mkpath(path);
0704 
0705     QJsonObject docObj;
0706     foreach (auto source, dataSources) {
0707         if (!isValidSource(source) || !source->isActive() || telemetryMode < source->telemetryMode())
0708             continue;
0709         QJsonObject obj;
0710         const auto data = source->data();
0711         if (data.canConvert<QVariantMap>())
0712             obj.insert(QLatin1String("data"), QJsonObject::fromVariantMap(data.toMap()));
0713         else if (data.canConvert<QVariantList>())
0714             obj.insert(QLatin1String("data"), QJsonArray::fromVariantList(data.value<QVariantList>()));
0715         if (obj.isEmpty())
0716             continue;
0717         obj.insert(QLatin1String("telemetryMode"), QString::fromLatin1(telemetryModeEnum().valueToKey(source->telemetryMode())));
0718         obj.insert(QLatin1String("description"), source->description());
0719         docObj.insert(source->id(), obj);
0720     }
0721 
0722     QFile file(path + QLatin1Char('/') + dt.toString(QStringLiteral("yyyyMMdd-hhmmss")) + QStringLiteral(".log"));
0723     if (!file.open(QFile::WriteOnly)) {
0724         qCWarning(Log) << "Unable to open audit log file:" << file.fileName() << file.errorString();
0725         return;
0726     }
0727 
0728     QJsonDocument doc(docObj);
0729     file.write(doc.toJson());
0730 
0731     qCDebug(Log) << "Audit log written:" << file.fileName();
0732 }
0733 
0734 QString Provider::describeDataSources() const
0735 {
0736     QString ret;
0737 
0738     const auto& mo = staticMetaObject;
0739     const int modeEnumIdx = mo.indexOfEnumerator("TelemetryMode");
0740     Q_ASSERT(modeEnumIdx >= 0);
0741 
0742     const auto modeEnum = mo.enumerator(modeEnumIdx);
0743     for (auto source : qAsConst(d->dataSources)) {
0744         ret += QString::fromUtf8(modeEnum.valueToKey(source->telemetryMode())) + QStringLiteral(": ") + source->name() + QLatin1Char('\n');
0745     }
0746     return ret;
0747 }
0748 
0749 #include "moc_provider.cpp"