File indexing completed on 2024-06-16 04:50:38

0001 /*
0002     SPDX-FileCopyrightText: 2008 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "selftestdialog.h"
0008 #include "agentmanager.h"
0009 #include "private/protocol_p.h"
0010 #include "private/standarddirs_p.h"
0011 #include "servermanager.h"
0012 #include "servermanager_p.h"
0013 
0014 #include <KLocalizedString>
0015 #include <KUser>
0016 #include <QFileDialog>
0017 #include <QIcon>
0018 #include <QMessageBox>
0019 #include <QSqlDatabase>
0020 #include <QSqlError>
0021 #include <QStandardPaths>
0022 #include <QUrl>
0023 
0024 #include <QApplication>
0025 #include <QClipboard>
0026 #include <QDBusConnection>
0027 #include <QDBusConnectionInterface>
0028 #include <QDate>
0029 #include <QDesktopServices>
0030 #include <QDialogButtonBox>
0031 #include <QFileInfo>
0032 #include <QProcess>
0033 #include <QPushButton>
0034 #include <QSettings>
0035 #include <QStandardItemModel>
0036 #include <QTextStream>
0037 #include <QVBoxLayout>
0038 
0039 /// @cond PRIVATE
0040 
0041 using namespace Akonadi;
0042 
0043 static QString makeLink(const QString &file)
0044 {
0045     return QStringLiteral("<a href=\"%1\">%2</a>").arg(file, file);
0046 }
0047 
0048 enum SelfTestRole {
0049     ResultTypeRole = Qt::UserRole,
0050     FileIncludeRole,
0051     ListDirectoryRole,
0052     EnvVarRole,
0053     SummaryRole,
0054     DetailsRole,
0055 };
0056 
0057 SelfTestDialog::SelfTestDialog(QWidget *parent)
0058     : QDialog(parent)
0059 {
0060     setWindowTitle(i18nc("@title:window", "Akonadi Server Self-Test"));
0061     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this);
0062     auto mainWidget = new QWidget(this);
0063     auto mainLayout = new QVBoxLayout(this);
0064     mainLayout->addWidget(mainWidget);
0065     auto user1Button = new QPushButton(this);
0066     buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole);
0067     auto user2Button = new QPushButton(this);
0068     buttonBox->addButton(user2Button, QDialogButtonBox::ActionRole);
0069     connect(buttonBox, &QDialogButtonBox::rejected, this, &SelfTestDialog::reject);
0070     mainLayout->addWidget(buttonBox);
0071     user1Button->setText(i18n("Save Report..."));
0072     user1Button->setIcon(QIcon::fromTheme(QStringLiteral("document-save")));
0073     user2Button->setText(i18n("Copy Report to Clipboard"));
0074     user2Button->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
0075     ui.setupUi(mainWidget);
0076 
0077     mTestModel = new QStandardItemModel(this);
0078     ui.testView->setModel(mTestModel);
0079     connect(ui.testView->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelfTestDialog::selectionChanged);
0080     connect(ui.detailsLabel, &QLabel::linkActivated, this, &SelfTestDialog::linkActivated);
0081 
0082     connect(user1Button, &QPushButton::clicked, this, &SelfTestDialog::saveReport);
0083     connect(user2Button, &QPushButton::clicked, this, &SelfTestDialog::copyReport);
0084 
0085     connect(ServerManager::self(), &ServerManager::stateChanged, this, &SelfTestDialog::runTests);
0086     runTests();
0087 }
0088 
0089 void SelfTestDialog::hideIntroduction()
0090 {
0091     ui.introductionLabel->hide();
0092 }
0093 
0094 QStandardItem *SelfTestDialog::report(ResultType type, const KLocalizedString &summary, const KLocalizedString &details)
0095 {
0096     auto item = new QStandardItem(summary.toString());
0097     switch (type) {
0098     case Skip:
0099         item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok")));
0100         break;
0101     case Success:
0102         item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply")));
0103         break;
0104     case Warning:
0105         item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning")));
0106         break;
0107     case Error:
0108         item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-error")));
0109         break;
0110     }
0111     item->setEditable(false);
0112     item->setWhatsThis(details.toString());
0113     item->setData(type, ResultTypeRole);
0114     item->setData(summary.toString(nullptr), SummaryRole);
0115     item->setData(details.toString(nullptr), DetailsRole);
0116     mTestModel->appendRow(item);
0117     return item;
0118 }
0119 
0120 void SelfTestDialog::selectionChanged(const QModelIndex &index)
0121 {
0122     if (index.isValid()) {
0123         ui.detailsLabel->setText(index.data(Qt::WhatsThisRole).toString());
0124         ui.detailsGroup->setEnabled(true);
0125     } else {
0126         ui.detailsLabel->setText(QString());
0127         ui.detailsGroup->setEnabled(false);
0128     }
0129 }
0130 
0131 void SelfTestDialog::runTests()
0132 {
0133     mTestModel->clear();
0134 
0135     const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString();
0136     testSQLDriver();
0137     if (driver == QLatin1StringView("QPSQL")) {
0138         testPSQLServer();
0139     } else {
0140 #ifndef Q_OS_WIN
0141         testRootUser();
0142 #endif
0143         testMySQLServer();
0144         testMySQLServerLog();
0145         testMySQLServerConfig();
0146     }
0147     testAkonadiCtl();
0148     testServerStatus();
0149     testProtocolVersion();
0150     testResources();
0151     testServerLog();
0152     testControlLog();
0153 }
0154 
0155 QVariant SelfTestDialog::serverSetting(const QString &group, const char *key, const QVariant &def) const
0156 {
0157     const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadOnly);
0158     QSettings settings(serverConfigFile, QSettings::IniFormat);
0159     settings.beginGroup(group);
0160     return settings.value(QString::fromLatin1(key), def);
0161 }
0162 
0163 bool SelfTestDialog::useStandaloneMysqlServer() const
0164 {
0165     const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString();
0166     if (driver != QLatin1StringView("QMYSQL")) {
0167         return false;
0168     }
0169     const bool startServer = serverSetting(driver, "StartServer", true).toBool();
0170     return startServer;
0171 }
0172 
0173 bool SelfTestDialog::runProcess(const QString &app, const QStringList &args, QString &result) const
0174 {
0175     QProcess proc;
0176     proc.start(app, args);
0177     const bool rv = proc.waitForFinished();
0178     result.clear();
0179     result = QString::fromLocal8Bit(proc.readAllStandardError());
0180     result += QString::fromLocal8Bit(proc.readAllStandardOutput());
0181     return rv;
0182 }
0183 
0184 void SelfTestDialog::testSQLDriver()
0185 {
0186     const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString();
0187     const QStringList availableDrivers = QSqlDatabase::drivers();
0188     const KLocalizedString detailsOk =
0189         ki18n("The QtSQL driver '%1' is required by your current Akonadi server configuration and was found on your system.").subs(driver);
0190     const KLocalizedString detailsFail = ki18n(
0191                                              "The QtSQL driver '%1' is required by your current Akonadi server configuration.\n"
0192                                              "The following drivers are installed: %2.\n"
0193                                              "Make sure the required driver is installed.")
0194                                              .subs(driver)
0195                                              .subs(availableDrivers.join(QLatin1StringView(", ")));
0196     QStandardItem *item = nullptr;
0197     if (availableDrivers.contains(driver)) {
0198         item = report(Success, ki18n("Database driver found."), detailsOk);
0199     } else {
0200         item = report(Error, ki18n("Database driver not found."), detailsFail);
0201     }
0202     item->setData(StandardDirs::serverConfigFile(StandardDirs::ReadOnly), FileIncludeRole);
0203 }
0204 
0205 void SelfTestDialog::testMySQLServer()
0206 {
0207     if (!useStandaloneMysqlServer()) {
0208         report(Skip, ki18n("MySQL server executable not tested."), ki18n("The current configuration does not require an internal MySQL server."));
0209         return;
0210     }
0211 
0212     const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString();
0213     const QString serverPath = serverSetting(driver, "ServerPath", QString()).toString(); // ### default?
0214 
0215     const KLocalizedString details = ki18n(
0216                                          "You have currently configured Akonadi to use the MySQL server '%1'.\n"
0217                                          "Make sure you have the MySQL server installed, set the correct path and ensure you have the "
0218                                          "necessary read and execution rights on the server executable. The server executable is typically "
0219                                          "called 'mysqld'; its location varies depending on the distribution.")
0220                                          .subs(serverPath);
0221 
0222     QFileInfo info(serverPath);
0223     if (!info.exists()) {
0224         report(Error, ki18n("MySQL server not found."), details);
0225     } else if (!info.isReadable()) {
0226         report(Error, ki18n("MySQL server not readable."), details);
0227     } else if (!info.isExecutable()) {
0228         report(Error, ki18n("MySQL server not executable."), details);
0229     } else if (!serverPath.contains(QLatin1StringView("mysqld"))) {
0230         report(Warning, ki18n("MySQL found with unexpected name."), details);
0231     } else {
0232         report(Success, ki18n("MySQL server found."), details);
0233     }
0234 
0235     // be extra sure and get the server version while we are at it
0236     QString result;
0237     if (runProcess(serverPath, QStringList() << QStringLiteral("--version"), result)) {
0238         const KLocalizedString details = ki18n("MySQL server found: %1").subs(result);
0239         report(Success, ki18n("MySQL server is executable."), details);
0240     } else {
0241         const KLocalizedString details = ki18n("Executing the MySQL server '%1' failed with the following error message: '%2'").subs(serverPath).subs(result);
0242         report(Error, ki18n("Executing the MySQL server failed."), details);
0243     }
0244 }
0245 
0246 void SelfTestDialog::testMySQLServerLog()
0247 {
0248     if (!useStandaloneMysqlServer()) {
0249         report(Skip, ki18n("MySQL server error log not tested."), ki18n("The current configuration does not require an internal MySQL server."));
0250         return;
0251     }
0252 
0253     const QString logFileName = StandardDirs::saveDir("data", QStringLiteral("db_data")) + QLatin1StringView("/mysql.err");
0254     const QFileInfo logFileInfo(logFileName);
0255     if (!logFileInfo.exists() || logFileInfo.size() == 0) {
0256         report(Success,
0257                ki18n("No current MySQL error log found."),
0258                ki18n("The MySQL server did not report any errors during this startup. The log can be found in '%1'.").subs(logFileName));
0259         return;
0260     }
0261     QFile logFile(logFileName);
0262     if (!logFile.open(QFile::ReadOnly | QFile::Text)) {
0263         report(Error,
0264                ki18n("MySQL error log not readable."),
0265                ki18n("A MySQL server error log file was found but is not readable: %1").subs(makeLink(logFileName)));
0266         return;
0267     }
0268     bool warningsFound = false;
0269     QStandardItem *item = nullptr;
0270     while (!logFile.atEnd()) {
0271         const QString line = QString::fromUtf8(logFile.readLine());
0272         if (line.contains(QLatin1StringView("error"), Qt::CaseInsensitive)) {
0273             item = report(Error,
0274                           ki18n("MySQL server log contains errors."),
0275                           ki18n("The MySQL server error log file '%1' contains errors.").subs(makeLink(logFileName)));
0276             item->setData(logFileName, FileIncludeRole);
0277             return;
0278         }
0279         if (!warningsFound && line.contains(QLatin1StringView("warn"), Qt::CaseInsensitive)) {
0280             warningsFound = true;
0281         }
0282     }
0283     if (warningsFound) {
0284         item = report(Warning,
0285                       ki18n("MySQL server log contains warnings."),
0286                       ki18n("The MySQL server log file '%1' contains warnings.").subs(makeLink(logFileName)));
0287     } else {
0288         item = report(Success,
0289                       ki18n("MySQL server log contains no errors."),
0290                       ki18n("The MySQL server log file '%1' does not contain any errors or warnings.").subs(makeLink(logFileName)));
0291     }
0292     item->setData(logFileName, FileIncludeRole);
0293 
0294     logFile.close();
0295 }
0296 
0297 void SelfTestDialog::testMySQLServerConfig()
0298 {
0299     if (!useStandaloneMysqlServer()) {
0300         report(Skip, ki18n("MySQL server configuration not tested."), ki18n("The current configuration does not require an internal MySQL server."));
0301         return;
0302     }
0303 
0304     QStandardItem *item = nullptr;
0305     const QString globalConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf"));
0306     const QFileInfo globalConfigInfo(globalConfig);
0307     if (!globalConfig.isEmpty() && globalConfigInfo.exists() && globalConfigInfo.isReadable()) {
0308         item = report(Success,
0309                       ki18n("MySQL server default configuration found."),
0310                       ki18n("The default configuration for the MySQL server was found and is readable at %1.").subs(makeLink(globalConfig)));
0311         item->setData(globalConfig, FileIncludeRole);
0312     } else {
0313         report(Error,
0314                ki18n("MySQL server default configuration not found."),
0315                ki18n("The default configuration for the MySQL server was not found or was not readable. "
0316                      "Check your Akonadi installation is complete and you have all required access rights."));
0317     }
0318 
0319     const QString localConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-local.conf"));
0320     const QFileInfo localConfigInfo(localConfig);
0321     if (localConfig.isEmpty() || !localConfigInfo.exists()) {
0322         report(Skip,
0323                ki18n("MySQL server custom configuration not available."),
0324                ki18n("The custom configuration for the MySQL server was not found but is optional."));
0325     } else if (localConfigInfo.exists() && localConfigInfo.isReadable()) {
0326         item = report(Success,
0327                       ki18n("MySQL server custom configuration found."),
0328                       ki18n("The custom configuration for the MySQL server was found and is readable at %1").subs(makeLink(localConfig)));
0329         item->setData(localConfig, FileIncludeRole);
0330     } else {
0331         report(Error,
0332                ki18n("MySQL server custom configuration not readable."),
0333                ki18n("The custom configuration for the MySQL server was found at %1 but is not readable. "
0334                      "Check your access rights.")
0335                    .subs(makeLink(localConfig)));
0336     }
0337 
0338     const QString actualConfig = StandardDirs::saveDir("data") + QStringLiteral("/mysql.conf");
0339     const QFileInfo actualConfigInfo(actualConfig);
0340     if (actualConfig.isEmpty() || !actualConfigInfo.exists() || !actualConfigInfo.isReadable()) {
0341         report(Error,
0342                ki18n("MySQL server configuration not found or not readable."),
0343                ki18n("The MySQL server configuration was not found or is not readable."));
0344     } else {
0345         item = report(Success,
0346                       ki18n("MySQL server configuration is usable."),
0347                       ki18n("The MySQL server configuration was found at %1 and is readable.").subs(makeLink(actualConfig)));
0348         item->setData(actualConfig, FileIncludeRole);
0349     }
0350 }
0351 
0352 void SelfTestDialog::testPSQLServer()
0353 {
0354     const QString dbname = serverSetting(QStringLiteral("QPSQL"), "Name", QStringLiteral("akonadi")).toString();
0355     const QString hostname = serverSetting(QStringLiteral("QPSQL"), "Host", QStringLiteral("localhost")).toString();
0356     const QString username = serverSetting(QStringLiteral("QPSQL"), "User", QString()).toString();
0357     const QString password = serverSetting(QStringLiteral("QPSQL"), "Password", QString()).toString();
0358     const int port = serverSetting(QStringLiteral("QPSQL"), "Port", 5432).toInt();
0359 
0360     QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL"));
0361     db.setHostName(hostname);
0362     db.setDatabaseName(dbname);
0363 
0364     if (!username.isEmpty()) {
0365         db.setUserName(username);
0366     }
0367 
0368     if (!password.isEmpty()) {
0369         db.setPassword(password);
0370     }
0371 
0372     db.setPort(port);
0373 
0374     if (!db.open()) {
0375         const KLocalizedString details = ki18n(db.lastError().text().toLatin1().constData());
0376         report(Error, ki18n("Cannot connect to PostgreSQL server."), details);
0377     } else {
0378         report(Success, ki18n("PostgreSQL server found."), ki18n("The PostgreSQL server was found and connection is working."));
0379     }
0380     db.close();
0381 }
0382 
0383 void SelfTestDialog::testAkonadiCtl()
0384 {
0385     const QString path = Akonadi::StandardDirs::findExecutable(QStringLiteral("akonadictl"));
0386     if (path.isEmpty()) {
0387         report(Error,
0388                ki18n("akonadictl not found"),
0389                ki18n("The program 'akonadictl' needs to be accessible in $PATH. "
0390                      "Make sure you have the Akonadi server installed."));
0391         return;
0392     }
0393     QString result;
0394     if (runProcess(path, QStringList() << QStringLiteral("--version"), result)) {
0395         report(Success,
0396                ki18n("akonadictl found and usable"),
0397                ki18n("The program '%1' to control the Akonadi server was found "
0398                      "and could be executed successfully.\nResult:\n%2")
0399                    .subs(path)
0400                    .subs(result));
0401     } else {
0402         report(Error,
0403                ki18n("akonadictl found but not usable"),
0404                ki18n("The program '%1' to control the Akonadi server was found "
0405                      "but could not be executed successfully.\nResult:\n%2\n"
0406                      "Make sure the Akonadi server is installed correctly.")
0407                    .subs(path)
0408                    .subs(result));
0409     }
0410 }
0411 
0412 void SelfTestDialog::testServerStatus()
0413 {
0414     if (QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control))) {
0415         report(Success,
0416                ki18n("Akonadi control process registered at D-Bus."),
0417                ki18n("The Akonadi control process is registered at D-Bus which typically indicates it is operational."));
0418     } else {
0419         report(Error,
0420                ki18n("Akonadi control process not registered at D-Bus."),
0421                ki18n("The Akonadi control process is not registered at D-Bus which typically means it was not started "
0422                      "or encountered a fatal error during startup."));
0423     }
0424 
0425     if (QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server))) {
0426         report(Success,
0427                ki18n("Akonadi server process registered at D-Bus."),
0428                ki18n("The Akonadi server process is registered at D-Bus which typically indicates it is operational."));
0429     } else {
0430         report(Error,
0431                ki18n("Akonadi server process not registered at D-Bus."),
0432                ki18n("The Akonadi server process is not registered at D-Bus which typically means it was not started "
0433                      "or encountered a fatal error during startup."));
0434     }
0435 }
0436 
0437 void SelfTestDialog::testProtocolVersion()
0438 {
0439     if (Internal::serverProtocolVersion() < 0) {
0440         report(Skip,
0441                ki18n("Protocol version check not possible."),
0442                ki18n("Without a connection to the server it is not possible to check if the protocol version meets the requirements."));
0443         return;
0444     }
0445     if (Internal::serverProtocolVersion() < Protocol::version()) {
0446         report(Error,
0447                ki18n("Server protocol version is too old."),
0448                ki18n("The server protocol version is %1, but version %2 is required by the client. "
0449                      "If you recently updated KDE PIM, please make sure to restart both Akonadi and KDE PIM applications.")
0450                    .subs(Internal::serverProtocolVersion())
0451                    .subs(Protocol::version()));
0452     } else if (Internal::serverProtocolVersion() > Protocol::version()) {
0453         report(Error,
0454                ki18n("Server protocol version is too new."),
0455                ki18n("The server protocol version is %1, but version %2 is required by the client. "
0456                      "If you recently updated KDE PIM, please make sure to restart both Akonadi and KDE PIM applications.")
0457                    .subs(Internal::serverProtocolVersion())
0458                    .subs(Protocol::version()));
0459     } else {
0460         report(Success, ki18n("Server protocol version matches."), ki18n("The current Protocol version is %1.").subs(Internal::serverProtocolVersion()));
0461     }
0462 }
0463 
0464 void SelfTestDialog::testResources()
0465 {
0466     const AgentType::List agentTypes = AgentManager::self()->types();
0467     bool resourceFound = false;
0468     for (const AgentType &type : agentTypes) {
0469         if (type.capabilities().contains(QLatin1StringView("Resource"))) {
0470             resourceFound = true;
0471             break;
0472         }
0473     }
0474 
0475     const auto pathList = StandardDirs::locateAllResourceDirs(QStringLiteral("akonadi/agents"));
0476     QStandardItem *item = nullptr;
0477     if (resourceFound) {
0478         item = report(Success, ki18n("Resource agents found."), ki18n("At least one resource agent has been found."));
0479     } else {
0480         item = report(Error,
0481                       ki18n("No resource agents found."),
0482                       ki18n("No resource agents have been found, Akonadi is not usable without at least one. "
0483                             "This usually means that no resource agents are installed or that there is a setup problem. "
0484                             "The following paths have been searched: '%1'. "
0485                             "The XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes all paths "
0486                             "where Akonadi agents are installed.")
0487                           .subs(pathList.join(QLatin1Char(' ')))
0488                           .subs(QString::fromLocal8Bit(qgetenv("XDG_DATA_DIRS"))));
0489     }
0490     item->setData(pathList, ListDirectoryRole);
0491     item->setData(QByteArray("XDG_DATA_DIRS"), EnvVarRole);
0492 }
0493 
0494 void SelfTestDialog::testServerLog()
0495 {
0496     QString serverLog = StandardDirs::saveDir("data") + QLatin1StringView("/akonadiserver.error");
0497     QFileInfo info(serverLog);
0498     if (!info.exists() || info.size() <= 0) {
0499         report(Success, ki18n("No current Akonadi server error log found."), ki18n("The Akonadi server did not report any errors during its current startup."));
0500     } else {
0501         QStandardItem *item =
0502             report(Error,
0503                    ki18n("Current Akonadi server error log found."),
0504                    ki18n("The Akonadi server reported errors during its current startup. The log can be found in %1.").subs(makeLink(serverLog)));
0505         item->setData(serverLog, FileIncludeRole);
0506     }
0507 
0508     serverLog += QStringLiteral(".old");
0509     info.setFile(serverLog);
0510     if (!info.exists() || info.size() <= 0) {
0511         report(Success,
0512                ki18n("No previous Akonadi server error log found."),
0513                ki18n("The Akonadi server did not report any errors during its previous startup."));
0514     } else {
0515         QStandardItem *item =
0516             report(Error,
0517                    ki18n("Previous Akonadi server error log found."),
0518                    ki18n("The Akonadi server reported errors during its previous startup. The log can be found in %1.").subs(makeLink(serverLog)));
0519         item->setData(serverLog, FileIncludeRole);
0520     }
0521 }
0522 
0523 void SelfTestDialog::testControlLog()
0524 {
0525     QString controlLog = StandardDirs::saveDir("data") + QLatin1StringView("/akonadi_control.error");
0526     QFileInfo info(controlLog);
0527     if (!info.exists() || info.size() <= 0) {
0528         report(Success,
0529                ki18n("No current Akonadi control error log found."),
0530                ki18n("The Akonadi control process did not report any errors during its current startup."));
0531     } else {
0532         QStandardItem *item =
0533             report(Error,
0534                    ki18n("Current Akonadi control error log found."),
0535                    ki18n("The Akonadi control process reported errors during its current startup. The log can be found in %1.").subs(makeLink(controlLog)));
0536         item->setData(controlLog, FileIncludeRole);
0537     }
0538 
0539     controlLog += QStringLiteral(".old");
0540     info.setFile(controlLog);
0541     if (!info.exists() || info.size() <= 0) {
0542         report(Success,
0543                ki18n("No previous Akonadi control error log found."),
0544                ki18n("The Akonadi control process did not report any errors during its previous startup."));
0545     } else {
0546         QStandardItem *item =
0547             report(Error,
0548                    ki18n("Previous Akonadi control error log found."),
0549                    ki18n("The Akonadi control process reported errors during its previous startup. The log can be found in %1.").subs(makeLink(controlLog)));
0550         item->setData(controlLog, FileIncludeRole);
0551     }
0552 }
0553 
0554 void SelfTestDialog::testRootUser()
0555 {
0556     KUser user;
0557     if (user.isSuperUser()) {
0558         report(Error,
0559                ki18n("Akonadi was started as root"),
0560                ki18n("Running Internet-facing applications as root/administrator exposes you to many security risks. MySQL, used by this Akonadi installation, "
0561                      "will not allow itself to run as root, to protect you from these risks."));
0562     } else {
0563         report(Success,
0564                ki18n("Akonadi is not running as root"),
0565                ki18n("Akonadi is not running as a root/administrator user, which is the recommended setup for a secure system."));
0566     }
0567 }
0568 
0569 QString SelfTestDialog::createReport()
0570 {
0571     QString result;
0572     QTextStream s(&result);
0573     s << "Akonadi Server Self-Test Report";
0574     s << "===============================";
0575 
0576     for (int i = 0; i < mTestModel->rowCount(); ++i) {
0577         QStandardItem *item = mTestModel->item(i);
0578         s << '\n';
0579         s << "Test " << (i + 1) << ":  ";
0580         switch (item->data(ResultTypeRole).toInt()) {
0581         case Skip:
0582             s << "SKIP";
0583             break;
0584         case Success:
0585             s << "SUCCESS";
0586             break;
0587         case Warning:
0588             s << "WARNING";
0589             break;
0590         case Error:
0591         default:
0592             s << "ERROR";
0593             break;
0594         }
0595         s << "\n--------\n";
0596         s << '\n';
0597         s << item->data(SummaryRole).toString() << '\n';
0598         s << "Details: " << item->data(DetailsRole).toString() << '\n';
0599         if (item->data(FileIncludeRole).isValid()) {
0600             s << '\n';
0601             const QString fileName = item->data(FileIncludeRole).toString();
0602             QFile f(fileName);
0603             if (f.open(QFile::ReadOnly)) {
0604                 s << "File content of '" << fileName << "':" << '\n';
0605                 s << f.readAll() << '\n';
0606             } else {
0607                 s << "File '" << fileName << "' could not be opened\n";
0608             }
0609         }
0610         if (item->data(ListDirectoryRole).isValid()) {
0611             s << '\n';
0612             const QStringList pathList = item->data(ListDirectoryRole).toStringList();
0613             if (pathList.isEmpty()) {
0614                 s << "Directory list is empty.\n";
0615             }
0616             for (const QString &path : pathList) {
0617                 s << "Directory listing of '" << path << "':\n";
0618                 QDir dir(path);
0619                 dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
0620                 const QStringList listEntries(dir.entryList());
0621                 for (const QString &entry : listEntries) {
0622                     s << entry << '\n';
0623                 }
0624             }
0625         }
0626         if (item->data(EnvVarRole).isValid()) {
0627             s << '\n';
0628             const QByteArray envVarName = item->data(EnvVarRole).toByteArray();
0629             const QByteArray envVarValue = qgetenv(envVarName.constData());
0630             s << "Environment variable " << envVarName << " is set to '" << envVarValue << "'\n";
0631         }
0632     }
0633 
0634     s << '\n';
0635     s.flush();
0636     return result;
0637 }
0638 
0639 void SelfTestDialog::saveReport()
0640 {
0641     const QString defaultFileName =
0642         QStringLiteral("akonadi-selftest-report-") + QDate::currentDate().toString(QStringLiteral("yyyyMMdd")) + QStringLiteral(".txt");
0643     const QString fileName = QFileDialog::getSaveFileName(this, i18n("Save Test Report"), defaultFileName);
0644     if (fileName.isEmpty()) {
0645         return;
0646     }
0647 
0648     QFile file(fileName);
0649     if (!file.open(QFile::ReadWrite)) {
0650         QMessageBox::critical(this, i18nc("@title:window", "Error"), i18n("Could not open file '%1'", fileName));
0651         return;
0652     }
0653 
0654     file.write(createReport().toUtf8());
0655     file.close();
0656 }
0657 
0658 void SelfTestDialog::copyReport()
0659 {
0660 #ifndef QT_NO_CLIPBOARD
0661     QApplication::clipboard()->setText(createReport());
0662 #endif
0663 }
0664 
0665 void SelfTestDialog::linkActivated(const QString &link)
0666 {
0667     QDesktopServices::openUrl(QUrl::fromLocalFile(link));
0668 }
0669 
0670 /// @endcond
0671 
0672 #include "moc_selftestdialog.cpp"