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