1 /*
2     SPDX-FileCopyrightText: 2008 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "selftestdialog.h"
8 #include "agentmanager.h"
9 #include "private/protocol_p.h"
10 #include "private/standarddirs_p.h"
11 #include "servermanager.h"
12 #include "servermanager_p.h"
13 
14 #include <KLocalizedString>
15 #include <KUser>
16 #include <QFileDialog>
17 #include <QIcon>
18 #include <QMessageBox>
19 #include <QSqlDatabase>
20 #include <QSqlError>
21 #include <QStandardPaths>
22 #include <QUrl>
23 
24 #include <QApplication>
25 #include <QClipboard>
26 #include <QDBusConnection>
27 #include <QDBusConnectionInterface>
28 #include <QDate>
29 #include <QDesktopServices>
30 #include <QDialogButtonBox>
31 #include <QFileInfo>
32 #include <QProcess>
33 #include <QPushButton>
34 #include <QSettings>
35 #include <QStandardItemModel>
36 #include <QTextStream>
37 #include <QVBoxLayout>
38 
39 /// @cond PRIVATE
40 
41 using namespace Akonadi;
42 
makeLink(const QString & file)43 static QString makeLink(const QString &file)
44 {
45     return QStringLiteral("<a href=\"%1\">%2</a>").arg(file, file);
46 }
47 
48 enum SelfTestRole {
49     ResultTypeRole = Qt::UserRole,
50     FileIncludeRole,
51     ListDirectoryRole,
52     EnvVarRole,
53     SummaryRole,
54     DetailsRole,
55 };
56 
SelfTestDialog(QWidget * parent)57 SelfTestDialog::SelfTestDialog(QWidget *parent)
58     : QDialog(parent)
59 {
60     setWindowTitle(i18nc("@title:window", "Akonadi Server Self-Test"));
61     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this);
62     auto mainWidget = new QWidget(this);
63     auto mainLayout = new QVBoxLayout(this);
64     mainLayout->addWidget(mainWidget);
65     auto user1Button = new QPushButton(this);
66     buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole);
67     auto user2Button = new QPushButton(this);
68     buttonBox->addButton(user2Button, QDialogButtonBox::ActionRole);
69     connect(buttonBox, &QDialogButtonBox::rejected, this, &SelfTestDialog::reject);
70     mainLayout->addWidget(buttonBox);
71     user1Button->setText(i18n("Save Report..."));
72     user1Button->setIcon(QIcon::fromTheme(QStringLiteral("document-save")));
73     user2Button->setText(i18n("Copy Report to Clipboard"));
74     user2Button->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
75     ui.setupUi(mainWidget);
76 
77     mTestModel = new QStandardItemModel(this);
78     ui.testView->setModel(mTestModel);
79     connect(ui.testView->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelfTestDialog::selectionChanged);
80     connect(ui.detailsLabel, &QLabel::linkActivated, this, &SelfTestDialog::linkActivated);
81 
82     connect(user1Button, &QPushButton::clicked, this, &SelfTestDialog::saveReport);
83     connect(user2Button, &QPushButton::clicked, this, &SelfTestDialog::copyReport);
84 
85     connect(ServerManager::self(), &ServerManager::stateChanged, this, &SelfTestDialog::runTests);
86     runTests();
87 }
88 
hideIntroduction()89 void SelfTestDialog::hideIntroduction()
90 {
91     ui.introductionLabel->hide();
92 }
93 
report(ResultType type,const KLocalizedString & summary,const KLocalizedString & details)94 QStandardItem *SelfTestDialog::report(ResultType type, const KLocalizedString &summary, const KLocalizedString &details)
95 {
96     auto item = new QStandardItem(summary.toString());
97     switch (type) {
98     case Skip:
99         item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok")));
100         break;
101     case Success:
102         item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply")));
103         break;
104     case Warning:
105         item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning")));
106         break;
107     case Error:
108         item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-error")));
109         break;
110     }
111     item->setEditable(false);
112     item->setWhatsThis(details.toString());
113     item->setData(type, ResultTypeRole);
114     item->setData(summary.toString(nullptr), SummaryRole);
115     item->setData(details.toString(nullptr), DetailsRole);
116     mTestModel->appendRow(item);
117     return item;
118 }
119 
selectionChanged(const QModelIndex & index)120 void SelfTestDialog::selectionChanged(const QModelIndex &index)
121 {
122     if (index.isValid()) {
123         ui.detailsLabel->setText(index.data(Qt::WhatsThisRole).toString());
124         ui.detailsGroup->setEnabled(true);
125     } else {
126         ui.detailsLabel->setText(QString());
127         ui.detailsGroup->setEnabled(false);
128     }
129 }
130 
runTests()131 void SelfTestDialog::runTests()
132 {
133     mTestModel->clear();
134 
135     const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString();
136     testSQLDriver();
137     if (driver == QLatin1String("QPSQL")) {
138         testPSQLServer();
139     } else {
140 #ifndef Q_OS_WIN
141         testRootUser();
142 #endif
143         testMySQLServer();
144         testMySQLServerLog();
145         testMySQLServerConfig();
146     }
147     testAkonadiCtl();
148     testServerStatus();
149     testProtocolVersion();
150     testResources();
151     testServerLog();
152     testControlLog();
153 }
154 
serverSetting(const QString & group,const char * key,const QVariant & def) const155 QVariant SelfTestDialog::serverSetting(const QString &group, const char *key, const QVariant &def) const
156 {
157     const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadOnly);
158     QSettings settings(serverConfigFile, QSettings::IniFormat);
159     settings.beginGroup(group);
160     return settings.value(QString::fromLatin1(key), def);
161 }
162 
useStandaloneMysqlServer() const163 bool SelfTestDialog::useStandaloneMysqlServer() const
164 {
165     const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString();
166     if (driver != QLatin1String("QMYSQL")) {
167         return false;
168     }
169     const bool startServer = serverSetting(driver, "StartServer", true).toBool();
170     return startServer;
171 }
172 
runProcess(const QString & app,const QStringList & args,QString & result) const173 bool SelfTestDialog::runProcess(const QString &app, const QStringList &args, QString &result) const
174 {
175     QProcess proc;
176     proc.start(app, args);
177     const bool rv = proc.waitForFinished();
178     result.clear();
179     result = QString::fromLocal8Bit(proc.readAllStandardError());
180     result += QString::fromLocal8Bit(proc.readAllStandardOutput());
181     return rv;
182 }
183 
testSQLDriver()184 void SelfTestDialog::testSQLDriver()
185 {
186     const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString();
187     const QStringList availableDrivers = QSqlDatabase::drivers();
188     const KLocalizedString detailsOk =
189         ki18n("The QtSQL driver '%1' is required by your current Akonadi server configuration and was found on your system.").subs(driver);
190     const KLocalizedString detailsFail = ki18n(
191                                              "The QtSQL driver '%1' is required by your current Akonadi server configuration.\n"
192                                              "The following drivers are installed: %2.\n"
193                                              "Make sure the required driver is installed.")
194                                              .subs(driver)
195                                              .subs(availableDrivers.join(QLatin1String(", ")));
196     QStandardItem *item = nullptr;
197     if (availableDrivers.contains(driver)) {
198         item = report(Success, ki18n("Database driver found."), detailsOk);
199     } else {
200         item = report(Error, ki18n("Database driver not found."), detailsFail);
201     }
202     item->setData(StandardDirs::serverConfigFile(StandardDirs::ReadOnly), FileIncludeRole);
203 }
204 
testMySQLServer()205 void SelfTestDialog::testMySQLServer()
206 {
207     if (!useStandaloneMysqlServer()) {
208         report(Skip, ki18n("MySQL server executable not tested."), ki18n("The current configuration does not require an internal MySQL server."));
209         return;
210     }
211 
212     const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString();
213     const QString serverPath = serverSetting(driver, "ServerPath", QString()).toString(); // ### default?
214 
215     const KLocalizedString details = ki18n(
216                                          "You have currently configured Akonadi to use the MySQL server '%1'.\n"
217                                          "Make sure you have the MySQL server installed, set the correct path and ensure you have the "
218                                          "necessary read and execution rights on the server executable. The server executable is typically "
219                                          "called 'mysqld'; its location varies depending on the distribution.")
220                                          .subs(serverPath);
221 
222     QFileInfo info(serverPath);
223     if (!info.exists()) {
224         report(Error, ki18n("MySQL server not found."), details);
225     } else if (!info.isReadable()) {
226         report(Error, ki18n("MySQL server not readable."), details);
227     } else if (!info.isExecutable()) {
228         report(Error, ki18n("MySQL server not executable."), details);
229     } else if (!serverPath.contains(QLatin1String("mysqld"))) {
230         report(Warning, ki18n("MySQL found with unexpected name."), details);
231     } else {
232         report(Success, ki18n("MySQL server found."), details);
233     }
234 
235     // be extra sure and get the server version while we are at it
236     QString result;
237     if (runProcess(serverPath, QStringList() << QStringLiteral("--version"), result)) {
238         const KLocalizedString details = ki18n("MySQL server found: %1").subs(result);
239         report(Success, ki18n("MySQL server is executable."), details);
240     } else {
241         const KLocalizedString details = ki18n("Executing the MySQL server '%1' failed with the following error message: '%2'").subs(serverPath).subs(result);
242         report(Error, ki18n("Executing the MySQL server failed."), details);
243     }
244 }
245 
testMySQLServerLog()246 void SelfTestDialog::testMySQLServerLog()
247 {
248     if (!useStandaloneMysqlServer()) {
249         report(Skip, ki18n("MySQL server error log not tested."), ki18n("The current configuration does not require an internal MySQL server."));
250         return;
251     }
252 
253     const QString logFileName = StandardDirs::saveDir("data", QStringLiteral("db_data")) + QLatin1String("/mysql.err");
254     const QFileInfo logFileInfo(logFileName);
255     if (!logFileInfo.exists() || logFileInfo.size() == 0) {
256         report(Success,
257                ki18n("No current MySQL error log found."),
258                ki18n("The MySQL server did not report any errors during this startup. The log can be found in '%1'.").subs(logFileName));
259         return;
260     }
261     QFile logFile(logFileName);
262     if (!logFile.open(QFile::ReadOnly | QFile::Text)) {
263         report(Error,
264                ki18n("MySQL error log not readable."),
265                ki18n("A MySQL server error log file was found but is not readable: %1").subs(makeLink(logFileName)));
266         return;
267     }
268     bool warningsFound = false;
269     QStandardItem *item = nullptr;
270     while (!logFile.atEnd()) {
271         const QString line = QString::fromUtf8(logFile.readLine());
272         if (line.contains(QLatin1String("error"), Qt::CaseInsensitive)) {
273             item = report(Error,
274                           ki18n("MySQL server log contains errors."),
275                           ki18n("The MySQL server error log file '%1' contains errors.").subs(makeLink(logFileName)));
276             item->setData(logFileName, FileIncludeRole);
277             return;
278         }
279         if (!warningsFound && line.contains(QLatin1String("warn"), Qt::CaseInsensitive)) {
280             warningsFound = true;
281         }
282     }
283     if (warningsFound) {
284         item = report(Warning,
285                       ki18n("MySQL server log contains warnings."),
286                       ki18n("The MySQL server log file '%1' contains warnings.").subs(makeLink(logFileName)));
287     } else {
288         item = report(Success,
289                       ki18n("MySQL server log contains no errors."),
290                       ki18n("The MySQL server log file '%1' does not contain any errors or warnings.").subs(makeLink(logFileName)));
291     }
292     item->setData(logFileName, FileIncludeRole);
293 
294     logFile.close();
295 }
296 
testMySQLServerConfig()297 void SelfTestDialog::testMySQLServerConfig()
298 {
299     if (!useStandaloneMysqlServer()) {
300         report(Skip, ki18n("MySQL server configuration not tested."), ki18n("The current configuration does not require an internal MySQL server."));
301         return;
302     }
303 
304     QStandardItem *item = nullptr;
305     const QString globalConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf"));
306     const QFileInfo globalConfigInfo(globalConfig);
307     if (!globalConfig.isEmpty() && globalConfigInfo.exists() && globalConfigInfo.isReadable()) {
308         item = report(Success,
309                       ki18n("MySQL server default configuration found."),
310                       ki18n("The default configuration for the MySQL server was found and is readable at %1.").subs(makeLink(globalConfig)));
311         item->setData(globalConfig, FileIncludeRole);
312     } else {
313         report(Error,
314                ki18n("MySQL server default configuration not found."),
315                ki18n("The default configuration for the MySQL server was not found or was not readable. "
316                      "Check your Akonadi installation is complete and you have all required access rights."));
317     }
318 
319     const QString localConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-local.conf"));
320     const QFileInfo localConfigInfo(localConfig);
321     if (localConfig.isEmpty() || !localConfigInfo.exists()) {
322         report(Skip,
323                ki18n("MySQL server custom configuration not available."),
324                ki18n("The custom configuration for the MySQL server was not found but is optional."));
325     } else if (localConfigInfo.exists() && localConfigInfo.isReadable()) {
326         item = report(Success,
327                       ki18n("MySQL server custom configuration found."),
328                       ki18n("The custom configuration for the MySQL server was found and is readable at %1").subs(makeLink(localConfig)));
329         item->setData(localConfig, FileIncludeRole);
330     } else {
331         report(Error,
332                ki18n("MySQL server custom configuration not readable."),
333                ki18n("The custom configuration for the MySQL server was found at %1 but is not readable. "
334                      "Check your access rights.")
335                    .subs(makeLink(localConfig)));
336     }
337 
338     const QString actualConfig = StandardDirs::saveDir("data") + QStringLiteral("/mysql.conf");
339     const QFileInfo actualConfigInfo(actualConfig);
340     if (actualConfig.isEmpty() || !actualConfigInfo.exists() || !actualConfigInfo.isReadable()) {
341         report(Error,
342                ki18n("MySQL server configuration not found or not readable."),
343                ki18n("The MySQL server configuration was not found or is not readable."));
344     } else {
345         item = report(Success,
346                       ki18n("MySQL server configuration is usable."),
347                       ki18n("The MySQL server configuration was found at %1 and is readable.").subs(makeLink(actualConfig)));
348         item->setData(actualConfig, FileIncludeRole);
349     }
350 }
351 
testPSQLServer()352 void SelfTestDialog::testPSQLServer()
353 {
354     const QString dbname = serverSetting(QStringLiteral("QPSQL"), "Name", QStringLiteral("akonadi")).toString();
355     const QString hostname = serverSetting(QStringLiteral("QPSQL"), "Host", QStringLiteral("localhost")).toString();
356     const QString username = serverSetting(QStringLiteral("QPSQL"), "User", QString()).toString();
357     const QString password = serverSetting(QStringLiteral("QPSQL"), "Password", QString()).toString();
358     const int port = serverSetting(QStringLiteral("QPSQL"), "Port", 5432).toInt();
359 
360     QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL"));
361     db.setHostName(hostname);
362     db.setDatabaseName(dbname);
363 
364     if (!username.isEmpty()) {
365         db.setUserName(username);
366     }
367 
368     if (!password.isEmpty()) {
369         db.setPassword(password);
370     }
371 
372     db.setPort(port);
373 
374     if (!db.open()) {
375         const KLocalizedString details = ki18n(db.lastError().text().toLatin1().constData());
376         report(Error, ki18n("Cannot connect to PostgreSQL server."), details);
377     } else {
378         report(Success, ki18n("PostgreSQL server found."), ki18n("The PostgreSQL server was found and connection is working."));
379     }
380     db.close();
381 }
382 
testAkonadiCtl()383 void SelfTestDialog::testAkonadiCtl()
384 {
385     const QString path = Akonadi::StandardDirs::findExecutable(QStringLiteral("akonadictl"));
386     if (path.isEmpty()) {
387         report(Error,
388                ki18n("akonadictl not found"),
389                ki18n("The program 'akonadictl' needs to be accessible in $PATH. "
390                      "Make sure you have the Akonadi server installed."));
391         return;
392     }
393     QString result;
394     if (runProcess(path, QStringList() << QStringLiteral("--version"), result)) {
395         report(Success,
396                ki18n("akonadictl found and usable"),
397                ki18n("The program '%1' to control the Akonadi server was found "
398                      "and could be executed successfully.\nResult:\n%2")
399                    .subs(path)
400                    .subs(result));
401     } else {
402         report(Error,
403                ki18n("akonadictl found but not usable"),
404                ki18n("The program '%1' to control the Akonadi server was found "
405                      "but could not be executed successfully.\nResult:\n%2\n"
406                      "Make sure the Akonadi server is installed correctly.")
407                    .subs(path)
408                    .subs(result));
409     }
410 }
411 
testServerStatus()412 void SelfTestDialog::testServerStatus()
413 {
414     if (QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control))) {
415         report(Success,
416                ki18n("Akonadi control process registered at D-Bus."),
417                ki18n("The Akonadi control process is registered at D-Bus which typically indicates it is operational."));
418     } else {
419         report(Error,
420                ki18n("Akonadi control process not registered at D-Bus."),
421                ki18n("The Akonadi control process is not registered at D-Bus which typically means it was not started "
422                      "or encountered a fatal error during startup."));
423     }
424 
425     if (QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server))) {
426         report(Success,
427                ki18n("Akonadi server process registered at D-Bus."),
428                ki18n("The Akonadi server process is registered at D-Bus which typically indicates it is operational."));
429     } else {
430         report(Error,
431                ki18n("Akonadi server process not registered at D-Bus."),
432                ki18n("The Akonadi server process is not registered at D-Bus which typically means it was not started "
433                      "or encountered a fatal error during startup."));
434     }
435 }
436 
testProtocolVersion()437 void SelfTestDialog::testProtocolVersion()
438 {
439     if (Internal::serverProtocolVersion() < 0) {
440         report(Skip,
441                ki18n("Protocol version check not possible."),
442                ki18n("Without a connection to the server it is not possible to check if the protocol version meets the requirements."));
443         return;
444     }
445     if (Internal::serverProtocolVersion() < Protocol::version()) {
446         report(Error,
447                ki18n("Server protocol version is too old."),
448                ki18n("The server protocol version is %1, but version %2 is required by the client. "
449                      "If you recently updated KDE PIM, please make sure to restart both Akonadi and KDE PIM applications.")
450                    .subs(Internal::serverProtocolVersion())
451                    .subs(Protocol::version()));
452     } else if (Internal::serverProtocolVersion() > Protocol::version()) {
453         report(Error,
454                ki18n("Server protocol version is too new."),
455                ki18n("The server protocol version is %1, but version %2 is required by the client. "
456                      "If you recently updated KDE PIM, please make sure to restart both Akonadi and KDE PIM applications.")
457                    .subs(Internal::serverProtocolVersion())
458                    .subs(Protocol::version()));
459     } else {
460         report(Success, ki18n("Server protocol version matches."), ki18n("The current Protocol version is %1.").subs(Internal::serverProtocolVersion()));
461     }
462 }
463 
testResources()464 void SelfTestDialog::testResources()
465 {
466     const AgentType::List agentTypes = AgentManager::self()->types();
467     bool resourceFound = false;
468     for (const AgentType &type : agentTypes) {
469         if (type.capabilities().contains(QLatin1String("Resource"))) {
470             resourceFound = true;
471             break;
472         }
473     }
474 
475     const auto pathList = StandardDirs::locateAllResourceDirs(QStringLiteral("akonadi/agents"));
476     QStandardItem *item = nullptr;
477     if (resourceFound) {
478         item = report(Success, ki18n("Resource agents found."), ki18n("At least one resource agent has been found."));
479     } else {
480         item = report(Error,
481                       ki18n("No resource agents found."),
482                       ki18n("No resource agents have been found, Akonadi is not usable without at least one. "
483                             "This usually means that no resource agents are installed or that there is a setup problem. "
484                             "The following paths have been searched: '%1'. "
485                             "The XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes all paths "
486                             "where Akonadi agents are installed.")
487                           .subs(pathList.join(QLatin1Char(' ')))
488                           .subs(QString::fromLocal8Bit(qgetenv("XDG_DATA_DIRS"))));
489     }
490     item->setData(pathList, ListDirectoryRole);
491     item->setData(QByteArray("XDG_DATA_DIRS"), EnvVarRole);
492 }
493 
testServerLog()494 void SelfTestDialog::testServerLog()
495 {
496     QString serverLog = StandardDirs::saveDir("data") + QLatin1String("/akonadiserver.error");
497     QFileInfo info(serverLog);
498     if (!info.exists() || info.size() <= 0) {
499         report(Success, ki18n("No current Akonadi server error log found."), ki18n("The Akonadi server did not report any errors during its current startup."));
500     } else {
501         QStandardItem *item =
502             report(Error,
503                    ki18n("Current Akonadi server error log found."),
504                    ki18n("The Akonadi server reported errors during its current startup. The log can be found in %1.").subs(makeLink(serverLog)));
505         item->setData(serverLog, FileIncludeRole);
506     }
507 
508     serverLog += QStringLiteral(".old");
509     info.setFile(serverLog);
510     if (!info.exists() || info.size() <= 0) {
511         report(Success,
512                ki18n("No previous Akonadi server error log found."),
513                ki18n("The Akonadi server did not report any errors during its previous startup."));
514     } else {
515         QStandardItem *item =
516             report(Error,
517                    ki18n("Previous Akonadi server error log found."),
518                    ki18n("The Akonadi server reported errors during its previous startup. The log can be found in %1.").subs(makeLink(serverLog)));
519         item->setData(serverLog, FileIncludeRole);
520     }
521 }
522 
testControlLog()523 void SelfTestDialog::testControlLog()
524 {
525     QString controlLog = StandardDirs::saveDir("data") + QLatin1String("/akonadi_control.error");
526     QFileInfo info(controlLog);
527     if (!info.exists() || info.size() <= 0) {
528         report(Success,
529                ki18n("No current Akonadi control error log found."),
530                ki18n("The Akonadi control process did not report any errors during its current startup."));
531     } else {
532         QStandardItem *item =
533             report(Error,
534                    ki18n("Current Akonadi control error log found."),
535                    ki18n("The Akonadi control process reported errors during its current startup. The log can be found in %1.").subs(makeLink(controlLog)));
536         item->setData(controlLog, FileIncludeRole);
537     }
538 
539     controlLog += QStringLiteral(".old");
540     info.setFile(controlLog);
541     if (!info.exists() || info.size() <= 0) {
542         report(Success,
543                ki18n("No previous Akonadi control error log found."),
544                ki18n("The Akonadi control process did not report any errors during its previous startup."));
545     } else {
546         QStandardItem *item =
547             report(Error,
548                    ki18n("Previous Akonadi control error log found."),
549                    ki18n("The Akonadi control process reported errors during its previous startup. The log can be found in %1.").subs(makeLink(controlLog)));
550         item->setData(controlLog, FileIncludeRole);
551     }
552 }
553 
testRootUser()554 void SelfTestDialog::testRootUser()
555 {
556     KUser user;
557     if (user.isSuperUser()) {
558         report(Error,
559                ki18n("Akonadi was started as root"),
560                ki18n("Running Internet-facing applications as root/administrator exposes you to many security risks. MySQL, used by this Akonadi installation, "
561                      "will not allow itself to run as root, to protect you from these risks."));
562     } else {
563         report(Success,
564                ki18n("Akonadi is not running as root"),
565                ki18n("Akonadi is not running as a root/administrator user, which is the recommended setup for a secure system."));
566     }
567 }
568 
createReport()569 QString SelfTestDialog::createReport()
570 {
571     QString result;
572     QTextStream s(&result);
573     s << "Akonadi Server Self-Test Report";
574     s << "===============================";
575 
576     for (int i = 0; i < mTestModel->rowCount(); ++i) {
577         QStandardItem *item = mTestModel->item(i);
578         s << '\n';
579         s << "Test " << (i + 1) << ":  ";
580         switch (item->data(ResultTypeRole).toInt()) {
581         case Skip:
582             s << "SKIP";
583             break;
584         case Success:
585             s << "SUCCESS";
586             break;
587         case Warning:
588             s << "WARNING";
589             break;
590         case Error:
591         default:
592             s << "ERROR";
593             break;
594         }
595         s << "\n--------\n";
596         s << '\n';
597         s << item->data(SummaryRole).toString() << '\n';
598         s << "Details: " << item->data(DetailsRole).toString() << '\n';
599         if (item->data(FileIncludeRole).isValid()) {
600             s << '\n';
601             const QString fileName = item->data(FileIncludeRole).toString();
602             QFile f(fileName);
603             if (f.open(QFile::ReadOnly)) {
604                 s << "File content of '" << fileName << "':" << '\n';
605                 s << f.readAll() << '\n';
606             } else {
607                 s << "File '" << fileName << "' could not be opened\n";
608             }
609         }
610         if (item->data(ListDirectoryRole).isValid()) {
611             s << '\n';
612             const QStringList pathList = item->data(ListDirectoryRole).toStringList();
613             if (pathList.isEmpty()) {
614                 s << "Directory list is empty.\n";
615             }
616             for (const QString &path : pathList) {
617                 s << "Directory listing of '" << path << "':\n";
618                 QDir dir(path);
619                 dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
620                 const QStringList listEntries(dir.entryList());
621                 for (const QString &entry : listEntries) {
622                     s << entry << '\n';
623                 }
624             }
625         }
626         if (item->data(EnvVarRole).isValid()) {
627             s << '\n';
628             const QByteArray envVarName = item->data(EnvVarRole).toByteArray();
629             const QByteArray envVarValue = qgetenv(envVarName.constData());
630             s << "Environment variable " << envVarName << " is set to '" << envVarValue << "'\n";
631         }
632     }
633 
634     s << '\n';
635     s.flush();
636     return result;
637 }
638 
saveReport()639 void SelfTestDialog::saveReport()
640 {
641     const QString defaultFileName =
642         QStringLiteral("akonadi-selftest-report-") + QDate::currentDate().toString(QStringLiteral("yyyyMMdd")) + QStringLiteral(".txt");
643     const QString fileName = QFileDialog::getSaveFileName(this, i18n("Save Test Report"), defaultFileName);
644     if (fileName.isEmpty()) {
645         return;
646     }
647 
648     QFile file(fileName);
649     if (!file.open(QFile::ReadWrite)) {
650         QMessageBox::critical(this, i18n("Error"), i18n("Could not open file '%1'", fileName));
651         return;
652     }
653 
654     file.write(createReport().toUtf8());
655     file.close();
656 }
657 
copyReport()658 void SelfTestDialog::copyReport()
659 {
660 #ifndef QT_NO_CLIPBOARD
661     QApplication::clipboard()->setText(createReport());
662 #endif
663 }
664 
linkActivated(const QString & link)665 void SelfTestDialog::linkActivated(const QString &link)
666 {
667     QDesktopServices::openUrl(QUrl::fromLocalFile(link));
668 }
669 
670 /// @endcond
671