view ui/mainwindow.cpp @ 445:b3721ded6f5b

Sort and filter the certificate list, changed certificate item roles.
author Raimund Renkert <rrenkert@intevation.de>
date Wed, 23 Apr 2014 11:53:57 +0200
parents 4b486a14252f
children 43090637d6ae
line wrap: on
line source
/* Copyright (C) 2014 by Bundesamt für Sicherheit in der Informationstechnik
 * Software engineering by Intevation GmbH
 *
 * This file is Free Software under the GNU GPL (v>=2)
 * and comes with ABSOLUTELY NO WARRANTY!
 * See LICENSE.txt for details.
 */
#include "mainwindow.h"

#include <QDebug>
#include <QProgressDialog>
#include <QMessageBox>
#include <QSystemTrayIcon>
#include <QAction>
#include <QDialog>
#include <QMenu>
#include <QApplication>
#include <QFile>
#include <QTimer>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGroupBox>
#include <QSplitter>
#include <QLabel>
#include <QImage>
#include <QCheckBox>

// The amount of time in minutes stay silent if we have
// something to say
#define NAG_INTERVAL_MINUTES 60

#ifndef APPNAME
#define APPNAME "TrustBridge"
#endif

#define SERVER_URL "https://files.intevation.de:443"
#define LIST_RESOURCE "/users/aheinecke/zertifikatsliste_1.txt"
#define SW_RESOURCE   "/users/aheinecke/zertifikatsliste_1.txt"

#include "certificatelist.h"
#include "downloader.h"
#include "helpdialog.h"
#include "aboutdialog.h"
#include "statusdialog.h"
#include "certificateitemdelegate.h"
#include "separatoritemdelegate.h"
#include "installwrapper.h"

MainWindow::MainWindow(bool trayMode):
    mTrayMode(trayMode)
{
    createActions();
    createTrayIcon();
    createMenuBar();
    createContent();
    resize(900, 540);
    qRegisterMetaType<SSLConnection::ErrorCode>("SSLConnection::ErrorCode");
    qRegisterMetaType<Certificate::Status>("Certificate::Status");

    connect(mTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
            this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));

    mMessageTimer = new QTimer(this);
    connect(mMessageTimer, SIGNAL(timeout()), this, SLOT(showMessage()));
    mMessageTimer->setInterval(NAG_INTERVAL_MINUTES * 60 * 1000);
    mMessageTimer->start();
    checkUpdates();
    loadUnselectedCertificates();
    loadCertificateList();
    if (!trayMode) {
        show();
    }
}

void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason)
{
    switch (reason) {
    case QSystemTrayIcon::Trigger:
    case QSystemTrayIcon::MiddleClick:
        showMessage();
        break;
    case QSystemTrayIcon::DoubleClick:
        show();
        break;
    default:
        ;
    }
}

void MainWindow::messageClicked()
{
    if (mCurState == NewListAvailable) {
        show();
    }
}

void MainWindow::showMessage()
{
    if (!isVisible() && !mCurMessage.isEmpty()) {
        mTrayIcon->showMessage(QApplication::applicationName(), mCurMessage,
                               QSystemTrayIcon::Information, 5000);
        mMessageTimer->start(); // Restart the timer so that we don't spam
    }
}

void MainWindow::verifyAvailableData()
{
    QString availableFileName = mSettings.value("List/available").toString();
    QString installedFileName = mSettings.value("List/installed").toString();
    QString swFileName = mSettings.value("Software/available").toString();

    if (!availableFileName.isEmpty()) {
        mListToInstall.readList(availableFileName.toLocal8Bit().constData());
        if (!mListToInstall.isValid()) {
            mCurState = TransferError;
            // Probably a bug when Qt fileName is encoded and cFileName
            // fails because of this. This needs a unit test!
            // Maybe check that the file is in our data directory
            QFile::remove(availableFileName);
            mSettings.remove("List/available");
            mSettings.remove("List/availableDate");
        }
    } else {
        // Make sure the available notation is also removed
        mSettings.remove("List/available");
        mSettings.remove("List/availableDate");
    }

    if (!installedFileName.isEmpty()) {
        mInstalledList.readList(installedFileName.toLocal8Bit().constData());
        if (!mInstalledList.isValid()) {
            // Probably a bug when Qt fileName is encoded and cFileName
            // fails because of this. This needs a unit test!
            // Maybe check that the file is in our data directory
            QFile::remove(installedFileName);
            mSettings.remove("List/installed");
            mSettings.remove("List/installedDate");
        }
    } else {
        // Make sure the available notation is also removed
        mSettings.remove("List/installed");
        mSettings.remove("List/installedDate");
    }

    if (!swFileName.isEmpty()) {
        // TODO
    } else {
        mSettings.remove("Software/available");
        mSettings.remove("Software/availableDate");
    }
}

void MainWindow::handleNewList(const QString& fileName, const QDateTime& modDate) {
    mSettings.setValue("List/available", fileName);
    mSettings.setValue("List/availableDate", modDate);

    verifyAvailableData();
    if (!mListToInstall.isValid()) {
        /* Downloader provided invalid files */
        /* TODO: Error count. Error handling. Otherwise
         * we can go into an endless loop here */

        /* Retry the download again in 10 - 20 minutes */
        QTimer::singleShot(600000 + (qrand() % 60000), this, SLOT(checkUpdates()));
    } else {
        mCurMessage = tr("An updated certificate list is available. Click here to install.");
        setState(NewListAvailable);
        showMessage();
        loadCertificateList();
    }
}

void MainWindow::handleNewSW(const QString& fileName, const QDateTime& modDate) {
    mCurMessage = tr("An update for %1 is available. Click here to install.").arg(
                QApplication::applicationName());
    setState(NewSoftwareAvailable);
    mSettings.setValue("Software/available", fileName);
    mSettings.setValue("Software/availableDate", modDate);

    mSettings.sync();
    showMessage();
}

void MainWindow::checkUpdates()
{
    verifyAvailableData();

    QDateTime listInstalledLastMod = mSettings.value("List/installedDate").toDateTime();
    QDateTime swInstalledLastMod = mSettings.value("Software/installedDate").toDateTime();

    QString listResource = QString::fromLatin1(LIST_RESOURCE);
    QString swResource = QString::fromLatin1(SW_RESOURCE);

#ifndef RELEASE_BUILD
    /* Use this for testing to specify another file name for updates */
    listResource = mSettings.value("List/resource", listResource).toString();
    swResource = mSettings.value("Software/resource", swResource).toString();
#endif


    Downloader* downloader = new Downloader(this,
                                            QString::fromLatin1(SERVER_URL),
                                            QByteArray(),
                                            QDateTime::currentDateTime(),
// TODO                                       swInstalledLastMod,
                                            listInstalledLastMod,
                                            swResource,
                                            listResource);

    connect(downloader, SIGNAL(newListAvailable(const QString&, const QDateTime&)),
            this, SLOT(handleNewList(const QString&, const QDateTime&)));
    connect(downloader, SIGNAL(newSoftwareAvailable(const QString&, const QDateTime&)),
            this, SLOT(handleNewSW(const QString&, const QDateTime&)));
    connect(downloader, SIGNAL(finished()), downloader, SLOT(deleteLater()));
    connect(downloader, SIGNAL(error(const QString &, SSLConnection::ErrorCode)),
            this, SLOT(downloaderError(const QString &, SSLConnection::ErrorCode)));
    downloader->start();
}


void MainWindow::downloaderError(const QString &message, SSLConnection::ErrorCode error)
{
    mCurMessage = message;
    showMessage();
}


void MainWindow::createActions()
{
    mCheckUpdates = new QAction(tr("Check for Updates"), this);
    connect(mCheckUpdates, SIGNAL(triggered()), this, SLOT(checkUpdates()));
    mQuitAction = new QAction(tr("Quit"), this);
    connect(mQuitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
}

void MainWindow::createTrayIcon()
{
    QIcon trayImg(":/img/tray_22.png");

    mTrayMenu = new QMenu(this);
    mTrayMenu->addAction(mCheckUpdates);
    mTrayMenu->addAction(mQuitAction);

    mTrayIcon = new QSystemTrayIcon(this);
    mTrayIcon->setContextMenu(mTrayMenu);

    mTrayIcon->setIcon(trayImg);
    setWindowIcon(trayImg);
    mTrayIcon->show();
    mTrayIcon->setToolTip(tr("TrustBridge"));

    connect(mTrayIcon, SIGNAL(messageClicked()), this, SLOT(messageClicked()));
}

void MainWindow::createMenuBar()
{
    mMenuBar = new QMenuBar(this);
    QMenu *mMenu = new QMenu(tr("Menu"), mMenuBar);
    mMenuBar->addMenu(mMenu);
    QAction *update = mMenu->addAction(tr("Force Update"));
    QAction *settings = mMenu->addAction(tr("Settings"));
    QAction *status = mMenu->addAction(tr("Statusdialog"));
    mMenu->addSeparator();
    QAction *help = mMenu->addAction(tr("Help"));
    QAction *about = mMenu->addAction(tr("About"));
    mMenu->addSeparator();
    QAction *quit = mMenu->addAction(tr("Quit"));
    connect(update, SIGNAL(triggered()), this, SLOT(checkUpdates()));
    connect(settings, SIGNAL(triggered()), this, SLOT(showSettings()));
    connect(status, SIGNAL(triggered()), this, SLOT(showStatus()));
    connect(help, SIGNAL(triggered()), this, SLOT(showHelp()));
    connect(about, SIGNAL(triggered()), this, SLOT(showAbout()));
    connect(quit, SIGNAL(triggered()), qApp, SLOT(quit()));
    setMenuBar(mMenuBar);
}

void MainWindow::createContent()
{
    // Create a central widget containing the main layout.
    QWidget *base = new QWidget;

    // Layouts and Container
    QHBoxLayout *mainLayout = new QHBoxLayout;
    QVBoxLayout *infoLayout = new QVBoxLayout;
    QVBoxLayout *certLayout = new QVBoxLayout;
    QHBoxLayout *detailLayout = new QHBoxLayout;
    QVBoxLayout *detailLabelLayout = new QVBoxLayout;
    QVBoxLayout *detailContentLayout = new QVBoxLayout;
    QHBoxLayout *headerLayout = new QHBoxLayout;
    QVBoxLayout *headerTextLayout = new QVBoxLayout;
    QHBoxLayout *bottomLayout = new QHBoxLayout;
    QVBoxLayout *settingsLayout = new QVBoxLayout;

    // The certificate list
    QGroupBox *certBox = new QGroupBox(tr("Managed Certificates"));
    mCertListWidget = new QListWidget;
    connect(mCertListWidget, SIGNAL(itemClicked(QListWidgetItem*)),
        this, SLOT(showDetails(QListWidgetItem*)));
    certLayout->addWidget(mCertListWidget);
    certBox->setLayout(certLayout);

    // The header (icon, about text)
    QImage *logoImage = new QImage(":/img/logo.png");
    QLabel *logo = new QLabel;
    logo->setBackgroundRole(QPalette::Base);
    logo->setPixmap(QPixmap::fromImage(*logoImage));
    QLabel *title = new QLabel("<h2>" + QString::fromLatin1(APPNAME) + "</h2>");
    QLabel *subTitle = new QLabel("This Software installs and removes Certificates");
    headerTextLayout->addWidget(title);
    headerTextLayout->addWidget(subTitle);
    headerLayout->addWidget(logo);
    headerLayout->addLayout(headerTextLayout);
    headerLayout->setStretch(0, 0);
    headerLayout->setStretch(1, 10);

    // The settings.
    QGroupBox *settingsBox = new QGroupBox(tr("Settings"));
    mAutoUpdateOption = new QCheckBox(tr("Autoupdate"));
    mAutoStartOption = new QCheckBox(tr("Autostart"));
    settingsLayout->addWidget(mAutoUpdateOption);
    settingsLayout->addWidget(mAutoStartOption);
    settingsBox->setLayout(settingsLayout);
//    connect(autoUpdateOption, SIGNAL(stateChanged()), this, SLOT(setAutoUpdate()));

    // The buttons.
    bottomLayout->setAlignment(Qt::AlignBottom);
    installButton = new QPushButton(tr("Install selected"));
    installButton->setFixedHeight(80);
    connect(installButton, SIGNAL(clicked()), this, SLOT(resizeButtons()));
    connect(installButton, SIGNAL(clicked()), this, SLOT(installCerts()));
    quitButton = new QPushButton(tr("Quit"));
    quitButton->setFixedHeight(20);
    connect(quitButton, SIGNAL(clicked()), qApp, SLOT(quit()));
    bottomLayout->insertStretch(0, 10);
    bottomLayout->addWidget(installButton);
    bottomLayout->setAlignment(installButton, Qt::AlignBottom);
    bottomLayout->addWidget(quitButton);
    bottomLayout->setAlignment(quitButton, Qt::AlignBottom);

    // The certificate details
    QGroupBox *detailBox = new QGroupBox(tr("Details"));
    QLabel *subjectCN = new QLabel(tr("Subject Common Name:"));
    QLabel *subjectOU = new QLabel(tr("Subject Organisation:"));
    QLabel *issuerCN = new QLabel(tr("Issuer Common Name:"));
    QLabel *issuerOU = new QLabel(tr("Issuer Organisation:"));
    QLabel *validFrom = new QLabel(tr("Valid from:"));
    QLabel *validTo = new QLabel(tr("Valid to:"));
    QLabel *fingerprint = new QLabel(tr("Fingerprint:"));
    detailLabelLayout->addWidget(subjectCN);
    detailLabelLayout->addWidget(subjectOU);
    detailLabelLayout->addWidget(issuerCN);
    detailLabelLayout->addWidget(issuerOU);
    detailLabelLayout->addWidget(validFrom);
    detailLabelLayout->addWidget(validTo);
    detailLabelLayout->addWidget(fingerprint);
    mSubjectCN = new QLabel(tr(""));
    mSubjectOU = new QLabel(tr(""));
    mIssuerCN = new QLabel(tr(""));
    mIssuerOU = new QLabel(tr(""));
    mValidFrom = new QLabel(tr(""));
    mValidTo = new QLabel(tr(""));
    mFingerprint = new QLabel(tr(""));
    detailContentLayout->addWidget(mSubjectCN);
    detailContentLayout->addWidget(mSubjectOU);
    detailContentLayout->addWidget(mIssuerCN);
    detailContentLayout->addWidget(mIssuerOU);
    detailContentLayout->addWidget(mValidFrom);
    detailContentLayout->addWidget(mValidTo);
    detailContentLayout->addWidget(mFingerprint);
    detailLayout->addLayout(detailLabelLayout);
    detailLayout->addLayout(detailContentLayout);
    detailBox->setLayout(detailLayout);

    infoLayout->addSpacing(20);
    infoLayout->addLayout(headerLayout);
    infoLayout->addWidget(detailBox);
    infoLayout->addWidget(settingsBox);
    infoLayout->addLayout(bottomLayout);

    mainLayout->addWidget(certBox);
    mainLayout->addLayout(infoLayout);


    // QMainWindow allready has a layout. All child layouts and widgets are
    // managed in the central widget.
    base->setLayout(mainLayout);
    setCentralWidget(base);
}

void MainWindow::loadCertificateList()
{
    mCertListWidget->clear();
    int i = 0;

    /* TODO: if nothing is available (neither old nor new) add some progress
     * indication */
    QList<Certificate> newInstallCerts;
    QList<Certificate> newRemoveCerts;
    QList<Certificate> oldInstallCerts;
    QList<Certificate> oldRemoveCerts;

    if (mListToInstall.getCertificates().isEmpty()) {
        // No new list available, add old certificates.
        foreach (const Certificate &cert, mInstalledList.getCertificates()) {
            if (cert.isInstallCert()) {
                oldInstallCerts.append(cert);
            }
            else {
                oldRemoveCerts.append(cert);
            }
        }
    }
    else {
        // Sort and filter both lists.
        foreach (const Certificate &cert, mListToInstall.getCertificates()) {
            if (cert.isInstallCert()) {
                // Certificate with status "install".
                if (mInstalledList.getCertificates().contains(cert)) {
                    // Was in the old list.
                    oldInstallCerts.append(cert);
                }
                else {
                    // Is a brand new certificate
                    newInstallCerts.append(cert);
                }
            }
            else {
                // Certificate with status "remove".
                if (mInstalledList.getCertificates().contains(cert)) {
                    // Was in the old list.
                    oldRemoveCerts.append(cert);
                }
                else {
                    // Was in the old list with status "install" and now has the
                    // status "remove".
                    newRemoveCerts.append(cert);
                }
            }
        }
    }

    // Add separators and certificates to list widget.
    mCertListWidget->addItem(createSeparator(tr("New certificates to install"), i++));
    foreach (const Certificate &cert, newInstallCerts) {
        mCertListWidget->addItem(createListItem(cert, Certificate::InstallNew, i++));
    }

    mCertListWidget->addItem(createSeparator(tr("New certificates to remove"), i++));
    foreach (const Certificate &cert, newRemoveCerts) {
        mCertListWidget->addItem(createListItem(cert, Certificate::RemoveNew, i++));
    }

    mCertListWidget->addItem(createSeparator(tr("Old certificates to install"), i++));
    foreach (const Certificate &cert, oldInstallCerts) {
        mCertListWidget->addItem(createListItem(cert, Certificate::InstallOld, i++));
    }

    mCertListWidget->addItem(createSeparator(tr("Old certificates to remove"), i++));
    foreach (const Certificate &cert, oldRemoveCerts) {
        mCertListWidget->addItem(createListItem(cert, Certificate::RemoveOld, i++));
    }
}

QListWidgetItem* MainWindow::createSeparator(const QString &text, int index)
{
    SeparatorItemDelegate *separatorDelegate = new SeparatorItemDelegate();
    QListWidgetItem *separator = new QListWidgetItem(text);
    mCertListWidget->setItemDelegateForRow(index, separatorDelegate);
    separator->setFlags(separator->flags() ^ Qt::ItemIsUserCheckable);
    return separator;
}

QListWidgetItem* MainWindow::createListItem(const Certificate &certificate,
    Certificate::Status status, int index)
{
    CertificateItemDelegate *certDelegate = new CertificateItemDelegate();
    QListWidgetItem* item = new QListWidgetItem(certificate.shortDescription());
    item->setData(CertificateItemDelegate::DataRole,
        QVariant::fromValue(certificate));
    item->setData(CertificateItemDelegate::StatusRole, status);
    if (!mPreviouslyUnselected.contains(certificate.base64Line()) &&
        status == Certificate::RemoveOld) {
        item->setFlags(item->flags() ^ Qt::ItemIsUserCheckable);
    }
    else {
        Qt::CheckState checkedState =
            mPreviouslyUnselected.contains(certificate.base64Line()) ?
                Qt::Unchecked : Qt::Checked;
        item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
        item->setCheckState(checkedState);
    }
    mCertListWidget->setItemDelegateForRow(index, certDelegate);
    return item;
}

void MainWindow::showSettings()
{
    qDebug() << "show settingsdialog";
}

void MainWindow::showStatus()
{
    qDebug() << "show settingsdialog";
    StatusDialog *status = new StatusDialog(this);
    status->show();
}

void MainWindow::showHelp()
{
    qDebug() << "show helpdialog";
    HelpDialog *help = new HelpDialog(this);
    help->show();
}

void MainWindow::showAbout()
{
    qDebug() << "show aboutdialog";
    AboutDialog *about = new AboutDialog(this);
    about->show();
}

void MainWindow::showDetails(QListWidgetItem *item)
{
    Certificate cert = item->data(CertificateItemDelegate::DataRole).value<Certificate>();
    mSubjectCN->setText(cert.subjectCN());
    mSubjectOU->setText(cert.subjectOU());
    mIssuerCN->setText(cert.issuerCN());
    mIssuerOU->setText(cert.issuerO());
    mValidFrom->setText(cert.validFrom().toString());
    mValidTo->setText(cert.validTo().toString());
    mFingerprint->setText(cert.fingerprint());
}

void MainWindow::resizeButtons()
{
    installButton->setFixedHeight(20);
    quitButton->setFixedHeight(80);
}

void MainWindow::installerError(const QString& errMsg) {
    QMessageBox::warning(this, tr("Error executing update"), errMsg);
}

void MainWindow::installerSuccess() {
    if (mCurState == NewListAvailable) {
        mCurState = NothingChanged;
        mCurMessage = QString();

        QString listFileName = mSettings.value("List/available").toString();
        QDateTime listFileDate = mSettings.value("List/availableDate").toDateTime();

        mSettings.remove("List/available");
        mSettings.remove("List/availableDate");

        if (listFileName.isEmpty() || !listFileDate.isValid()) {
            qWarning() << "Error accessing settings";
            return; /* Try again with next check */
        }

        mSettings.setValue("List/installed", listFileName);
        mSettings.setValue("List/installedDate", listFileDate);
    }
}

void MainWindow::installCerts() {
    QStringList choices;

    for (int i = 0; i < mCertListWidget->count(); i++) {
        QListWidgetItem *item = mCertListWidget->item(i);
        if (item->checkState() == Qt::Checked) {
            choices << item->data(CertificateItemDelegate::DataRole).value<Certificate>().base64Line();
            continue;
        }
        QString certLine = item->data(CertificateItemDelegate::DataRole).value<Certificate>().base64Line();
        if (certLine.startsWith("I:")) {
            certLine[0] = 'R';
            choices << certLine;
        }
    }

    QProgressDialog *progress = new QProgressDialog(this);
    progress->setWindowModality(Qt::WindowModal);
    progress->setLabelText(tr("Installing certificates..."));
    progress->setCancelButton(0);
    progress->setRange(0,0);
    progress->setMinimumDuration(0);
    progress->show();

    InstallWrapper *instWrap = new InstallWrapper(this,
                                                  mListToInstall.isValid() ?
                                                  mListToInstall.fileName() :
                                                  mInstalledList.fileName(),
                                                  choices);
    /* Clean up object and progress dialog */
    connect(instWrap, SIGNAL(finished()), instWrap, SLOT(deleteLater()));
    connect(instWrap, SIGNAL(finished()), progress, SLOT(deleteLater()));
    connect(instWrap, SIGNAL(finished()), progress, SLOT(cancel()));
    connect(instWrap, SIGNAL(installationSuccessful()),
            this, SLOT(installerSuccess()));
    connect(instWrap, SIGNAL(error(const QString &)),
            this, SLOT(installerError(const QString &)));
    instWrap->start();

    if (!saveUnselectedCertificates()) {
        qWarning() << "Failed to save previosly unselected certificates.";
    }
}

void MainWindow::loadUnselectedCertificates()
{
    mPreviouslyUnselected.clear();
    mSettings.beginGroup("unselected");
    QStringList keys = mSettings.allKeys();
    foreach (const QString &key, keys) {
        mPreviouslyUnselected << mSettings.value(key, QString()).toString();
    }
    mSettings.endGroup();
}

bool MainWindow::saveUnselectedCertificates()
{
    mSettings.beginGroup("unselected");
    mSettings.remove(""); /* Clears old choices */
    for (int i = 0; i < mCertListWidget->count(); i++) {
        QListWidgetItem *item = mCertListWidget->item(i);
        if (item->checkState() != Qt::Checked) {
            mSettings.setValue(QString::fromLatin1("cert%1").arg(i),
                    item->data(CertificateItemDelegate::DataRole).value<Certificate>().base64Line());
        }
    }
    mSettings.endGroup();
    mSettings.sync();
    return mSettings.status() == QSettings::NoError;
}

http://wald.intevation.org/projects/trustbridge/