File indexing completed on 2024-05-12 16:40:07

0001 /* This file is part of the KDE project
0002    Copyright (C) 2012 Jarosław Staniek <staniek@kde.org>
0003 
0004    This library is free software; you can redistribute it and/or
0005    modify it under the terms of the GNU Library General Public
0006    License as published by the Free Software Foundation; either
0007    version 2 of the License, or (at your option) any later version.
0008 
0009    This library is distributed in the hope that it will be useful,
0010    but WITHOUT ANY WARRANTY; without even the implied warranty of
0011    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0012    Library General Public License for more details.
0013 
0014    You should have received a copy of the GNU Library General Public License
0015    along with this library; see the file COPYING.LIB.  If not, write to
0016    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0017  * Boston, MA 02110-1301, USA.
0018 */
0019 
0020 #include "KexiUserFeedbackAgent.h"
0021 
0022 #include <KexiVersion.h>
0023 #include <KexiMainWindowIface.h>
0024 #include <kexiutils/utils.h>
0025 
0026 #include <KIO/Job>
0027 #include <KConfigGroup>
0028 #include <kwidgetsaddons_version.h> // no kdeversion.h anymore but this is nice enough here
0029 #include <KSharedConfig>
0030 #include <KLocalizedString>
0031 
0032 #include <QDebug>
0033 #include <QApplication>
0034 #include <QDesktopWidget>
0035 #include <QProcess>
0036 #include <QUuid>
0037 #include <QLocale>
0038 
0039 #if defined Q_OS_WIN
0040 #include <windows.h>
0041 #include <stdio.h>
0042 #endif
0043 
0044 #if defined HAVE_UNAME
0045 # include <sys/utsname.h>
0046 #endif
0047 
0048 /*! Version of the feedback data format.
0049  Changelog:
0050  1.0: initial version
0051  1.1: added JSON-compatible character escaping; added uid
0052  1.2: added os_release, os_machine, screen_count
0053  1.3: added running_desktop, running_desktop_version
0054 */
0055 static const char KexiUserFeedbackAgent_VERSION[] = "1.3";
0056 
0057 class Q_DECL_HIDDEN KexiUserFeedbackAgent::Private
0058 {
0059 public:
0060     Private()
0061      : configGroup(KConfigGroup(KSharedConfig::openConfig()->group("User Feedback")))
0062      , areas(KexiUserFeedbackAgent::NoAreas)
0063      , sentDataInThisSession(KexiUserFeedbackAgent::NoAreas)
0064      , url(QLatin1String("http://www.kexi-project.org/feedback"))
0065      , redirectChecked(false)
0066     {
0067     }
0068 
0069     void updateData();
0070 
0071     KConfigGroup configGroup;
0072     KexiUserFeedbackAgent::Areas areas;
0073     KexiUserFeedbackAgent::Areas sentDataInThisSession;
0074     QList<QByteArray> keys;
0075     QMap<QByteArray, QVariant> data;
0076     QMap<QByteArray, KexiUserFeedbackAgent::Area> areasForKeys;
0077     //! Unique user ID handy if used does not want to disclose username
0078     //! but agrees to be identified as unique user of the application.
0079     QUuid uid;
0080     QString url;
0081     bool redirectChecked;
0082 };
0083 
0084 void KexiUserFeedbackAgent::Private::updateData()
0085 {
0086     keys.clear();
0087     data.clear();
0088     areasForKeys.clear();
0089     #define ADD(key, val, area) { \
0090         keys.append(QByteArray(key)); data.insert(QByteArray(key), QVariant(val)); \
0091         areasForKeys.insert(key, KexiUserFeedbackAgent::area); \
0092     }
0093     ADD("ver", KexiUserFeedbackAgent_VERSION, BasicArea);
0094     ADD("uid", uid.toString(), AnonymousIdentificationArea);
0095     ADD("app_ver", Kexi::versionString(), BasicArea);
0096     ADD("app_ver_major", Kexi::versionMajor(), BasicArea);
0097     ADD("app_ver_minor", Kexi::versionMinor(), BasicArea);
0098     ADD("app_ver_release", Kexi::versionRelease(), BasicArea);
0099 
0100     //! @TODO KEXI3 For kde_ver_* use runtime information like KDE::versionMajor(), not the macro (KF5 libs lack it unfortunately)
0101     ADD("kde_ver", KWIDGETSADDONS_VERSION_STRING, BasicArea);
0102     ADD("kde_ver_major", KWIDGETSADDONS_VERSION_MAJOR, BasicArea);
0103     ADD("kde_ver_minor", KWIDGETSADDONS_VERSION_MINOR, BasicArea);
0104     ADD("kde_ver_release", KWIDGETSADDONS_VERSION_PATCH, BasicArea);
0105 #ifdef Q_OS_LINUX
0106     {
0107         ADD("os", "linux", SystemInfoArea);
0108         const QByteArray desktop = KexiUtils::detectedDesktopSession();
0109         const QByteArray gdm = qgetenv("GDMSESSION").trimmed().toUpper();
0110         const bool kdeSession = qgetenv("KDE_FULL_SESSION").trimmed().toLower() == "true";
0111         // detect running desktop
0112         // https://standards.freedesktop.org/menu-spec/latest/apb.html
0113         QString runningDesktop;
0114         QString runningDesktopVersion;
0115         //! @todo set runningDesktopVersion for other desktops
0116         if (desktop.contains("KDE") || kdeSession || gdm.contains("KDE")) {
0117             runningDesktop = "KDE Plasma";
0118             //! @todo run kde{4|5}-config --kde-version to know full version and save in runningDesktopVersion
0119             runningDesktopVersion = qgetenv("KDE_SESSION_VERSION").trimmed();
0120         }
0121         else if (desktop.contains("UNITY")) {
0122             runningDesktop = "Unity";
0123         }
0124         else if (desktop.contains("RAZOR")) {
0125             runningDesktop = "Razor-qt";
0126         }
0127         else if (desktop.contains("ROX")) {
0128             runningDesktop = "ROX";
0129         }
0130         else if (desktop.contains("TDE")) {
0131             runningDesktop = "Trinity";
0132         }
0133         else if (desktop.contains("MATE")) {
0134             runningDesktop = "MATE";
0135         }
0136         else if (desktop.contains("LXDE") || gdm.contains("LUBUNTU")) {
0137             runningDesktop = "LXDE";
0138         }
0139         else if (desktop.contains("XFCE") || gdm.contains("XFCE")) {
0140             runningDesktop = "Xfce";
0141         }
0142         else if (desktop.contains("EDE")) {
0143             runningDesktop = "EDE";
0144         }
0145         else if (desktop.contains("CINNAMON")) {
0146             runningDesktop = "Cinnamon";
0147         }
0148         else if (desktop.contains("GNOME") || gdm.contains("GNOME")) {
0149             if (gdm.contains("cinnamon")) {
0150                 runningDesktop = "Cinnamon";
0151             }
0152             else if (gdm.contains("CLASSIC")) {
0153                 runningDesktop = "GNOME Classic";
0154             }
0155             else {
0156                 runningDesktop = "GNOME";
0157             }
0158         }
0159         else {
0160             if (!desktop.isEmpty()) {
0161                 runningDesktop = "Other: " + desktop.toLower();
0162             }
0163             else if (!gdm.isEmpty()) {
0164                 runningDesktop = "Other: " + gdm.toLower();
0165             }
0166         }
0167         if (!runningDesktop.isEmpty()) {
0168             ADD("running_desktop", runningDesktop, SystemInfoArea);
0169         }
0170         if (!runningDesktopVersion.isEmpty()) {
0171             ADD("running_desktop_version", runningDesktopVersion, SystemInfoArea);
0172         }
0173         // retrieve distribution name and release
0174         QProcess p;
0175         p.start("lsb_release", QStringList() << "-i" << "-r" << "-d");
0176         if (p.waitForFinished()) {
0177             QString info = p.readLine().replace("Distributor ID:", "").trimmed();
0178             if (info.toLower() == "ubuntu") { // Ubuntu derivatives (https://askubuntu.com/a/227669/226642)
0179                 if (runningDesktop == "KDE Plasma") {
0180                     info = "Kubuntu";
0181                 }
0182                 else if (runningDesktop ==  "LXDE") {
0183                     info = "Lubuntu";
0184                 }
0185                 else if (runningDesktop == "Xfce") {
0186                     info = "Xubuntu";
0187                 }
0188                 else if (runningDesktop.contains(QLatin1String("gnome"), Qt::CaseInsensitive)) {
0189                     info = "Ubuntu GNOME";
0190                 }
0191             }
0192             if (!info.isEmpty()) {
0193                 ADD("linux_id", info, SystemInfoArea);
0194             }
0195             info = p.readLine().replace("Description:", "").trimmed();
0196             if (!info.isEmpty()) {
0197                 ADD("linux_desc", info, SystemInfoArea);
0198             }
0199             info = p.readLine().replace("Release:", "").trimmed();
0200             if (!info.isEmpty()) {
0201                 ADD("linux_rel", info, SystemInfoArea);
0202             }
0203         }
0204         p.close();
0205     }
0206 #elif defined(Q_OS_MACOS)
0207     ADD("os", "mac", SystemInfoArea);
0208 #elif defined(Q_OS_WIN)
0209     ADD("os", "windows", SystemInfoArea);
0210 #else
0211 //! @todo BSD?
0212     ADD("os", "other", SystemInfoArea);
0213 #endif
0214 
0215 #if defined HAVE_UNAME
0216     struct utsname buf;
0217     if (uname(&buf) == 0) {
0218         ADD("os_release", buf.release, SystemInfoArea);
0219         ADD("os_machine", buf.machine, SystemInfoArea);
0220     }
0221 #elif defined(Q_OS_WIN)
0222     OSVERSIONINFO versionInfo;
0223     SYSTEM_INFO sysInfo;
0224     char* releaseStr;
0225     releaseStr = new char[6]; // "xx.xx\0"
0226 
0227     versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
0228 #pragma warning(push)
0229 #pragma warning(disable: 4996) // easy way, VerifyVersionInfo() is not a 100% equivalent or easy to use
0230     GetVersionEx(&versionInfo);
0231 #pragma warning(pop)
0232     GetSystemInfo(&sysInfo);
0233 
0234     _snprintf(releaseStr, 6, "%2d.%2d", versionInfo.dwMajorVersion, versionInfo.dwMinorVersion);
0235     ADD("os_release", releaseStr, SystemInfoArea);
0236 
0237     delete [6] releaseStr;
0238 
0239     switch(sysInfo.wProcessorArchitecture) {
0240     case PROCESSOR_ARCHITECTURE_AMD64:
0241         ADD("os_machine", "x86_64", SystemInfoArea);
0242         break;
0243     case PROCESSOR_ARCHITECTURE_IA64:
0244         ADD("os_machine", "ia64", SystemInfoArea);
0245         break;
0246     case PROCESSOR_ARCHITECTURE_INTEL:
0247         ADD("os_machine", "x86", SystemInfoArea);
0248         break;
0249     default:
0250         ADD("os_machine", "unknown", SystemInfoArea);
0251     }
0252 #endif
0253 
0254     QSize screen(QApplication::desktop()->screenGeometry(
0255                         KexiMainWindowIface::global()->thisWidget()).size());
0256     ADD("screen_width", screen.width(), ScreenInfoArea);
0257     ADD("screen_height", screen.height(), ScreenInfoArea);
0258     ADD("screen_count", QApplication::desktop()->screenCount(), ScreenInfoArea);
0259 
0260     QLocale locale;
0261     ADD("country", QLocale::countryToString(locale.country()), RegionalSettingsArea);
0262     ADD("language", QLocale::languageToString(locale.language()), RegionalSettingsArea);
0263     ADD("date_format", locale.dateFormat(QLocale::LongFormat), RegionalSettingsArea);
0264     ADD("short_date_format", locale.dateFormat(QLocale::ShortFormat), RegionalSettingsArea);
0265     ADD("time_format", locale.timeFormat(QLocale::LongFormat), RegionalSettingsArea);
0266     //! @todo KEXI3 "short_time_format"
0267     ADD("right_to_left", QApplication::isRightToLeft(), RegionalSettingsArea);
0268 #undef ADD
0269 }
0270 
0271 // ---
0272 
0273 KexiUserFeedbackAgent::KexiUserFeedbackAgent(QObject* parent)
0274  : QObject(parent), d(new Private)
0275 {
0276     if (d->configGroup.readEntry("BasicInfo", false)) {
0277         d->areas |= BasicArea | AnonymousIdentificationArea;
0278     }
0279     if (d->configGroup.readEntry("SystemInfo", false)) {
0280         d->areas |= SystemInfoArea;
0281     }
0282     if (d->configGroup.readEntry("ScreenInfo", false)) {
0283         d->areas |= ScreenInfoArea;
0284     }
0285     if (d->configGroup.readEntry("RegionalSettings", false)) {
0286         d->areas |= RegionalSettingsArea;
0287     }
0288 
0289     // load or create uid
0290     QString uidString = d->configGroup.readEntry("Uid", QString());
0291     d->uid = QUuid(uidString);
0292     if (d->uid.isNull()) {
0293         d->uid = QUuid::createUuid();
0294         d->configGroup.writeEntry("Uid", d->uid.toString());
0295     }
0296 
0297     d->updateData();
0298     sendData();
0299 }
0300 
0301 KexiUserFeedbackAgent::~KexiUserFeedbackAgent()
0302 {
0303     delete d;
0304 }
0305 
0306 void KexiUserFeedbackAgent::setEnabledAreas(Areas areas)
0307 {
0308     if (areas != NoAreas && areas != AllAreas && !(areas & BasicArea)) {
0309         areas |= BasicArea; // fix, Basic Info is required
0310     }
0311     if (d->areas == areas) {
0312         return;
0313     }
0314     d->areas = areas;
0315     d->configGroup.writeEntry("BasicInfo", bool(d->areas & BasicArea));
0316     d->configGroup.writeEntry("SystemInfo", bool(d->areas & SystemInfoArea));
0317     d->configGroup.writeEntry("ScreenInfo", bool(d->areas & ScreenInfoArea));
0318     d->configGroup.writeEntry("RegionalSettings", bool(d->areas & RegionalSettingsArea));
0319     d->configGroup.sync();
0320     if ((d->sentDataInThisSession & d->areas) != d->areas) {
0321         d->updateData();
0322         sendData();
0323     }
0324 }
0325 
0326 KexiUserFeedbackAgent::Areas  KexiUserFeedbackAgent::enabledAreas() const
0327 {
0328     return d->areas;
0329 }
0330 
0331 //! Escapes string for JSON format (see https://json.org).
0332 inline QString escapeJson(const QString& s)
0333 {
0334     QString res;
0335     for (int i=0; i<s.length(); i++) {
0336         switch (s[i].toLatin1()) {
0337         case '\\': res += QLatin1String("\\\\"); break;
0338         case '/': res += QLatin1String("\\/"); break;
0339         case '"': res += QLatin1String("\\\""); break;
0340         case '\b': res += QLatin1String("\\b"); break;
0341         case '\f': res += QLatin1String("\\f"); break;
0342         case '\n': res += QLatin1String("\\n"); break;
0343         case '\r': res += QLatin1String("\\r"); break;
0344         case '\t': res += QLatin1String("\\t"); break;
0345         default: res += s[i];
0346         }
0347     }
0348     return res;
0349 }
0350 
0351 void KexiUserFeedbackAgent::sendData()
0352 {
0353     if (d->areas == NoAreas) {
0354         return;
0355     }
0356     if (!Kexi::isKexiInstance()) {
0357         // Do not send feedback if this is not really Kexi but a test app based on Kexi
0358         return;
0359     }
0360     if (!d->redirectChecked) {
0361         sendRedirectQuestion();
0362         return;
0363     }
0364 
0365     QByteArray postData;
0366     foreach (const QByteArray& key, d->keys) {
0367         Area area = d->areasForKeys.value(key);
0368         if (area != NoAreas && (d->areas & area)) {
0369             if (!postData.isEmpty()) {
0370                 postData += ',';
0371             }
0372             postData += (QByteArray("\"") + key + "\":\""
0373                         + escapeJson(d->data.value(key).toString()).toUtf8() + '"');
0374         }
0375     }
0376     //qDebug() << postData;
0377 
0378     KIO::Job* sendJob = KIO::storedHttpPost(postData, QUrl(d->url + "/send"), KIO::HideProgressInfo);
0379     connect(sendJob, SIGNAL(result(KJob*)), this, SLOT(sendDataFinished(KJob*)));
0380     sendJob->addMetaData("content-type", "Content-Type: application/x-www-form-urlencoded");
0381 }
0382 
0383 void KexiUserFeedbackAgent::sendDataFinished(KJob* job)
0384 {
0385     if (job->error()) {
0386         //! @todo error...
0387         return;
0388     }
0389     KIO::StoredTransferJob* sendJob = qobject_cast<KIO::StoredTransferJob*>(job);
0390     QByteArray result = sendJob->data();
0391     result.chop(1); // remove \n
0392     //qDebug() << result;
0393     if (result == "ok") {
0394         d->sentDataInThisSession = d->areas;
0395     }
0396 }
0397 
0398 QVariant KexiUserFeedbackAgent::value(const QString& key) const
0399 {
0400     return d->data.value(key.toLatin1());
0401 }
0402 
0403 void KexiUserFeedbackAgent::sendRedirectQuestion()
0404 {
0405     QByteArray postData = "get_url";
0406     //qDebug() << postData;
0407     KIO::Job* sendJob = KIO::storedHttpPost(postData, QUrl(d->url + "/send"), KIO::HideProgressInfo);
0408     connect(sendJob, SIGNAL(result(KJob*)), this, SLOT(sendRedirectQuestionFinished(KJob*)));
0409     sendJob->addMetaData("content-type", "Content-Type: application/x-www-form-urlencoded");
0410 }
0411 
0412 void KexiUserFeedbackAgent::sendRedirectQuestionFinished(KJob* job)
0413 {
0414     if (job->error()) {
0415         //! @todo error...
0416         qWarning() << "Error, no URL Redirect";
0417     }
0418     else {
0419         KIO::StoredTransferJob* sendJob = qobject_cast<KIO::StoredTransferJob*>(job);
0420         QByteArray result = sendJob->data();
0421         result.chop(1); // remove \n
0422         //qDebug() << result;
0423         if (result.isEmpty()) {
0424             //qDebug() << "No URL Redirect";
0425         }
0426         else {
0427             d->url = QString::fromUtf8(result);
0428             //qDebug() << "URL Redirect to" << d->url;
0429         }
0430     }
0431     d->redirectChecked = true;
0432     emit redirectLoaded();
0433     sendData();
0434 }
0435 
0436 QString KexiUserFeedbackAgent::serviceUrl() const
0437 {
0438     return d->url;
0439 }
0440 
0441 void KexiUserFeedbackAgent::waitForRedirect(QObject *receiver, const char* slot)
0442 {
0443     if (!receiver) {
0444         return;
0445     }
0446     if (d->redirectChecked) {
0447         QMetaObject::invokeMethod(receiver, slot);
0448     }
0449     else {
0450         connect(this, SIGNAL(redirectLoaded()), receiver, slot, Qt::UniqueConnection);
0451         if (d->areas == NoAreas) {
0452             sendRedirectQuestion();
0453         }
0454     }
0455 }