diff --git a/src/ui/directoryserviceswidget.cpp b/src/ui/directoryserviceswidget.cpp index fbcc72e7b..b2a70d35b 100644 --- a/src/ui/directoryserviceswidget.cpp +++ b/src/ui/directoryserviceswidget.cpp @@ -1,722 +1,742 @@ /* directoryserviceswidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2001, 2002, 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2017 Bundesamnt für Sicherheit in der Informationstechnik SPDX-License-Identifier: GPL-2.0-or-later */ #include "directoryserviceswidget.h" #include "ui_directoryserviceswidget.h" #include "kleo_ui_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace Kleo; namespace { static QUrl defaultX509Service() { QUrl url; url.setScheme(QStringLiteral("ldap")); url.setHost(i18nc("default server name, keep it a valid domain name, ie. no spaces", "server")); return url; } static QUrl defaultOpenPGPService() { QUrl url; if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() < "2.1.16") { url.setScheme(QStringLiteral("hkp")); url.setHost(QStringLiteral("keys.gnupg.net")); } else { url.setScheme(QStringLiteral("hkps")); url.setHost(QStringLiteral("hkps.pool.sks-keyservers.net")); } return url; } static bool is_ldap_scheme(const QUrl &url) { const QString scheme = url.scheme(); return QString::compare(scheme, QStringLiteral("ldap"), Qt::CaseInsensitive) == 0 || QString::compare(scheme, QStringLiteral("ldaps"), Qt::CaseInsensitive) == 0; } static const struct { const char label[6]; unsigned short port; DirectoryServicesWidget::Scheme base; } protocols[] = { { I18N_NOOP("hkp"), 11371, DirectoryServicesWidget::HKP }, { I18N_NOOP("http"), 80, DirectoryServicesWidget::HTTP }, { I18N_NOOP("https"), 443, DirectoryServicesWidget::HTTP }, { I18N_NOOP("ftp"), 21, DirectoryServicesWidget::FTP }, { I18N_NOOP("ftps"), 990, DirectoryServicesWidget::FTP }, { I18N_NOOP("ldap"), 389, DirectoryServicesWidget::LDAP }, { I18N_NOOP("ldaps"), 636, DirectoryServicesWidget::LDAP }, }; static const unsigned int numProtocols = sizeof protocols / sizeof * protocols; static unsigned short default_port(const QString &scheme) { for (unsigned int i = 0; i < numProtocols; ++i) if (QString::compare(scheme, QLatin1String(protocols[i].label), Qt::CaseInsensitive) == 0) { return protocols[i].port; } return 0; } static QString display_scheme(const QUrl &url) { if (url.scheme().isEmpty()) { return QStringLiteral("hkp"); } else { return url.scheme(); } } static QString display_host(const QUrl &url) { // work around "subkeys.pgp.net" being interpreted as a path, not host if (url.host().isEmpty()) { return url.path(); } else { return url.host(); } } static unsigned short display_port(const QUrl &url) { if (url.port() > 0) { return url.port(); } else { return default_port(display_scheme(url)); } } static QRect calculate_geometry(const QRect &cell, const QSize &sizeHint) { const int height = qMax(cell.height(), sizeHint.height()); return {cell.left(), cell.top() - (height - cell.height()) / 2, cell.width(), height}; } /* The Model contains a bit historic cruft because in the past it was * thought to be a good idea to combine openPGP and X509 in a single * table although while you can have multiple X509 Keyservers there can * only be one OpenPGP Keyserver. So the OpenPGP Keyserver is now a * single lineedit. */ class Model : public QAbstractTableModel { Q_OBJECT public: explicit Model(QObject *parent = nullptr) : QAbstractTableModel(parent), m_items(), m_x509ReadOnly(false), m_schemes(DirectoryServicesWidget::LDAP) { } void setX509ReadOnly(bool ro) { if (ro == m_x509ReadOnly) { return; } m_x509ReadOnly = ro; for (int row = 0, end = rowCount(); row != end; ++row) { Q_EMIT dataChanged(index(row, 0), index(row, NumColumns)); } } QModelIndex addX509Service(const QUrl &url, bool force = false) { const auto it = force ? m_items.end() : findExistingUrl(url); unsigned int row; if (it != m_items.end()) { // existing item: row = it - m_items.begin(); Q_EMIT dataChanged(index(row, 0), index(row, NumColumns)); } else { // append new item row = m_items.size(); beginInsertRows(QModelIndex(), row, row); m_items.push_back(url); endInsertRows(); } return index(row, firstEditableColumn(row)); } unsigned int numServices() const { return m_items.size(); } QUrl service(unsigned int row) const { return row < m_items.size() ? m_items[row] : QUrl(); } enum Columns { Host, Port, BaseDN, UserName, Password, + Flags, - NumColumns + NumColumns, }; QModelIndex duplicateRow(unsigned int row) { if (row >= m_items.size()) { return {}; } beginInsertRows(QModelIndex(), row + 1, row + 1); m_items.insert(m_items.begin() + row + 1, m_items[row]); endInsertRows(); return index(row + 1, 0); } void deleteRow(unsigned int row) { if (row >= m_items.size()) { return; } beginRemoveRows(QModelIndex(), row, row); m_items.erase(m_items.begin() + row); endInsertRows(); } void clear() { if (m_items.empty()) { return; } beginRemoveRows(QModelIndex(), 0, m_items.size() - 1); m_items.clear(); endRemoveRows(); } int columnCount(const QModelIndex & = QModelIndex()) const override { return NumColumns; } int rowCount(const QModelIndex & = QModelIndex()) const override { return m_items.size(); } QVariant data(const QModelIndex &idx, int role) const override; QVariant headerData(int section, Qt::Orientation o, int role) const override; Qt::ItemFlags flags(const QModelIndex &idx) const override; bool setData(const QModelIndex &idx, const QVariant &value, int role) override; private: bool doSetData(unsigned int row, unsigned int column, const QVariant &value, int role); static QString toolTipForColumn(int column); bool isLdapRow(unsigned int row) const; int firstEditableColumn(unsigned int) const { return Host; } private: std::vector m_items; bool m_x509ReadOnly : 1; DirectoryServicesWidget::Schemes m_schemes; private: std::vector::iterator findExistingUrl(const QUrl &url) { return std::find_if(m_items.begin(), m_items.end(), [&url](const QUrl &item) { const QUrl &lhs = url; const QUrl &rhs = item; return QString::compare(display_scheme(lhs), display_scheme(rhs), Qt::CaseInsensitive) == 0 && QString::compare(display_host(lhs), display_host(rhs), Qt::CaseInsensitive) == 0 && lhs.port() == rhs.port() && lhs.userName() == rhs.userName() // ... ignore password... && (!is_ldap_scheme(lhs) || lhs.query() == rhs.query()); }); } }; class Delegate : public QItemDelegate { Q_OBJECT public: explicit Delegate(QObject *parent = nullptr) : QItemDelegate(parent), m_schemes(DirectoryServicesWidget::LDAP) { } void setAllowedSchemes(const DirectoryServicesWidget::Schemes schemes) { m_schemes = schemes; } DirectoryServicesWidget::Schemes allowedSchemes() const { return m_schemes; } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &idx) const override { switch (idx.column()) { case Model::Port: return createPortWidget(parent); } return QItemDelegate::createEditor(parent, option, idx); } void setEditorData(QWidget *editor, const QModelIndex &idx) const override { switch (idx.column()) { case Model::Port: setPortEditorData(qobject_cast(editor), idx.data(Qt::EditRole).toInt()); break; default: QItemDelegate::setEditorData(editor, idx); break; } } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &idx) const override { switch (idx.column()) { case Model::Port: setPortModelData(qobject_cast(editor), model, idx); break; default: QItemDelegate::setModelData(editor, model, idx); break; } } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == Model::Port) { editor->setGeometry(calculate_geometry(option.rect, editor->sizeHint())); } else { QItemDelegate::updateEditorGeometry(editor, option, index); } } private: QWidget *createPortWidget(QWidget *parent) const { auto sb = new QSpinBox(parent); sb->setRange(1, USHRT_MAX); // valid port numbers return sb; } void setPortEditorData(QSpinBox *sb, unsigned short port) const { Q_ASSERT(sb); sb->setValue(port); } void setPortModelData(const QSpinBox *sb, QAbstractItemModel *model, const QModelIndex &idx) const { Q_ASSERT(sb); Q_ASSERT(model); model->setData(idx, sb->value()); } private: DirectoryServicesWidget::Schemes m_schemes; }; } class DirectoryServicesWidget::Private { friend class ::Kleo::DirectoryServicesWidget; DirectoryServicesWidget *const q; public: explicit Private(DirectoryServicesWidget *qq) : q(qq), protocols(AllProtocols), readOnlyProtocols(NoProtocol), model(), delegate(), ui(q) { ui.treeView->setModel(&model); ui.treeView->setItemDelegate(&delegate); ui.pgpKeyserver->setPlaceholderText(defaultOpenPGPService().toString()); connect(&model, &QAbstractItemModel::dataChanged, q, &DirectoryServicesWidget::changed); connect(&model, &QAbstractItemModel::rowsInserted, q, &DirectoryServicesWidget::changed); connect(&model, &QAbstractItemModel::rowsRemoved, q, &DirectoryServicesWidget::changed); connect(ui.treeView->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this]() { slotSelectionChanged(); }); connect(ui.pgpKeyserver, &QLineEdit::textChanged, q, &DirectoryServicesWidget::changed); slotShowUserAndPasswordToggled(false); } private: void edit(const QModelIndex &index) { if (index.isValid()) { ui.treeView->clearSelection(); ui.treeView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); ui.treeView->edit(index); } } void slotNewX509Clicked() { edit(model.addX509Service(defaultX509Service(), true)); } void slotDeleteClicked() { model.deleteRow(selectedRow()); } void slotSelectionChanged() { enableDisableActions(); } void slotShowUserAndPasswordToggled(bool on) { QHeaderView *const hv = ui.treeView->header(); Q_ASSERT(hv); hv->setSectionHidden(Model::UserName, !on); hv->setSectionHidden(Model::Password, !on); } int selectedRow() const { const QModelIndexList mil = ui.treeView->selectionModel()->selectedRows(); return mil.empty() ? -1 : mil.front().row(); } int currentRow() const { const QModelIndex idx = ui.treeView->selectionModel()->currentIndex(); return idx.isValid() ? idx.row() : -1; } void enableDisableActions() { const bool x509 = (protocols & X509Protocol) && !(readOnlyProtocols & X509Protocol); const bool pgp = (protocols & OpenPGPProtocol) && !(readOnlyProtocols & OpenPGPProtocol); ui.newTB->setEnabled(x509); ui.pgpKeyserver->setEnabled(pgp); const int row = selectedRow(); ui.deleteTB->setEnabled(row >= 0 && !(readOnlyProtocols & X509Protocol)); } private: Protocols protocols; Protocols readOnlyProtocols; Model model; Delegate delegate; struct UI : Ui_DirectoryServicesWidget { explicit UI(DirectoryServicesWidget *q) : Ui_DirectoryServicesWidget() { setupUi(q); } } ui; }; DirectoryServicesWidget::DirectoryServicesWidget(QWidget *p, Qt::WindowFlags f) : QWidget(p, f), d(new Private(this)) { } DirectoryServicesWidget::~DirectoryServicesWidget() { delete d; } void DirectoryServicesWidget::setAllowedSchemes(Schemes schemes) { d->delegate.setAllowedSchemes(schemes); } DirectoryServicesWidget::Schemes DirectoryServicesWidget::allowedSchemes() const { return d->delegate.allowedSchemes(); } void DirectoryServicesWidget::setAllowedProtocols(Protocols protocols) { if (d->protocols == protocols) { return; } d->protocols = protocols; d->enableDisableActions(); } DirectoryServicesWidget::Protocols DirectoryServicesWidget::allowedProtocols() const { return d->protocols; } void DirectoryServicesWidget::setReadOnlyProtocols(Protocols protocols) { if (d->readOnlyProtocols == protocols) { return; } d->readOnlyProtocols = protocols; d->model.setX509ReadOnly(protocols & X509Protocol); d->enableDisableActions(); } DirectoryServicesWidget::Protocols DirectoryServicesWidget::readOnlyProtocols() const { return d->readOnlyProtocols; } void DirectoryServicesWidget::setOpenPGPService(const QString &url) { d->ui.pgpKeyserver->setText(url); } QString DirectoryServicesWidget::openPGPService() const { const auto pgpStr = d->ui.pgpKeyserver->text(); return pgpStr.contains(QLatin1String("://")) ? pgpStr : (QLatin1String("hkps://") + pgpStr); } void DirectoryServicesWidget::addX509Services(const QList &urls) { for (const QUrl &url : urls) { d->model.addX509Service(url); } } QList DirectoryServicesWidget::x509Services() const { QList result; unsigned int numServices{d->model.numServices()}; result.reserve(numServices); for (unsigned int i = 0; i != numServices; ++i) { result.push_back(d->model.service(i)); } return result; } void DirectoryServicesWidget::clear() { if (!d->model.numServices()) { return; } d->model.clear(); d->ui.pgpKeyserver->setText(QString()); Q_EMIT changed(); } // // Model // QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) if (role == Qt::ToolTipRole) { return toolTipForColumn(section); } else if (role == Qt::DisplayRole) switch (section) { case Host: return i18n("Server Name"); case Port: return i18n("Server Port"); case BaseDN: return i18n("Base DN"); case UserName: return i18n("User Name"); case Password: return i18n("Password"); + case Flags: return i18nc("Flags to enable/disable certain features", "Flags"); default: return QVariant(); } else { return QVariant(); } else { return QAbstractTableModel::headerData(section, orientation, role); } } QVariant Model::data(const QModelIndex &index, int role) const { const unsigned int row = index.row(); if (index.isValid() && row < m_items.size()) switch (role) { case Qt::ToolTipRole: { const QString tt = toolTipForColumn(index.column()); if (!m_x509ReadOnly) { return tt; } else return tt.isEmpty() ? i18n("(read-only)") : i18nc("amended tooltip; %1: original tooltip", "%1 (read-only)", tt); } case Qt::DisplayRole: case Qt::EditRole: switch (index.column()) { case Host: return display_host(m_items[row]); case Port: return display_port(m_items[row]); case BaseDN: if (isLdapRow(row)) { return m_items[row].query(); } else { return QVariant(); } case UserName: return m_items[row].userName(); case Password: return m_items[row].password(); + case Flags: + return m_items[row].fragment(); default: return QVariant(); } } return QVariant(); } bool Model::isLdapRow(unsigned int row) const { if (row >= m_items.size()) { return false; } return is_ldap_scheme(m_items[row]); } Qt::ItemFlags Model::flags(const QModelIndex &index) const { const unsigned int row = index.row(); Qt::ItemFlags flags = QAbstractTableModel::flags(index); if (m_x509ReadOnly) { flags &= ~Qt::ItemIsSelectable; } if (index.isValid() && row < m_items.size()) switch (index.column()) { case Host: case Port: if (m_x509ReadOnly) { return flags & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled); } else { return flags | Qt::ItemIsEditable; } case BaseDN: if (isLdapRow(row) && !m_x509ReadOnly) { return flags | Qt::ItemIsEditable; } else { return flags & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled); } case UserName: case Password: if (m_x509ReadOnly) { return flags & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled); } else { return flags | Qt::ItemIsEditable; } + case Flags: + if (m_x509ReadOnly) { + return flags & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled); + } else { + return flags | Qt::ItemIsEditable; + } } + return flags; } bool Model::setData(const QModelIndex &idx, const QVariant &value, int role) { const unsigned int row = idx.row(); if (!idx.isValid() || row >= m_items.size()) { return false; } if (m_x509ReadOnly) { return false; } if (!doSetData(row, idx.column(), value, role)) { return false; } Q_EMIT dataChanged(idx, idx); return true; } bool Model::doSetData(unsigned int row, unsigned int column, const QVariant &value, int role) { if (role == Qt::EditRole) switch (column) { case Host: if (display_host(m_items[row]) != m_items[row].host()) { m_items[row].setScheme(display_scheme(m_items[row])); } m_items[row].setHost(value.toString()); return true; case Port: if (value.toUInt() == default_port(display_scheme(m_items[row]))) { m_items[row].setPort(-1); } else { m_items[row].setPort(value.toUInt()); } return true; case BaseDN: if (value.toString().isEmpty()) { m_items[row].setPath(QString()); m_items[row].setQuery(QString()); } else { m_items[row].setQuery(value.toString()); } return true; case UserName: m_items[row].setUserName(value.toString()); return true; case Password: m_items[row].setPassword(value.toString()); return true; + case Flags: + if (value.toString().isEmpty()) { + m_items[row].setFragment(QString()); // unset the fragment + } else { + m_items[row].setFragment(value.toString()); + } + return true; } return false; } // static QString Model::toolTipForColumn(int column) { switch (column) { case Host: return i18n("Enter the name or IP address of the server " "hosting the directory service."); case Port: return i18n("(Optional, the default is fine in most cases) " "Pick the port number the directory service is " "listening on."); case BaseDN: return i18n("(Only for LDAP) " "Enter the base DN for this LDAP server to " "limit searches to only that subtree of the directory."); case UserName: return i18n("(Optional) " "Enter your user name here, if needed."); case Password: return i18n("(Optional, not recommended) " "Enter your password here, if needed. " "Note that the password will be saved in the clear " "in a config file in your home directory."); + case Flags: return i18n("(Optional) " + "Enter 'ldaps' to specify that a TLS connections shall be used."); default: return QString(); } } #include "directoryserviceswidget.moc" #include "moc_directoryserviceswidget.cpp"