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 }